From ede7ff9c56c46fec8089c9dd079fcb8aa22c38d8 Mon Sep 17 00:00:00 2001 From: Prajon Date: Thu, 9 Jul 2020 14:05:46 -0400 Subject: [PATCH 01/21] Added code for decoding before encoding query params and standardize path --- CHANGELOG.md | 4 ++ src/Medidata.MAuth.Core/MAuthAuthenticator.cs | 15 +++-- .../MAuthCoreExtensions.cs | 45 +++++++++++++- src/Medidata.MAuth.Core/MAuthCoreV2.cs | 9 ++- .../MAuthCoreExtensionsTests.cs | 60 ++++++++++++++++++- .../Medidata.MAuth.Tests/MAuthCoreV2Tests.cs | 2 +- version.props | 2 +- 7 files changed, 123 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 336b97a..9b76829 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changes in Medidata.MAuth +## v4.0.3 + - **[Core]** Added normalization of Uri AbsolutePath. + - **[Core]** Added unescape step in query_string encoding to remove `double encoding`. + ## v4.0.2 - **[AspNetCore]** Update aspnetcore version to aspnetcore2.1 LTS. - **[Core]** Fallback to V1 protocol when V2 athentication fails. diff --git a/src/Medidata.MAuth.Core/MAuthAuthenticator.cs b/src/Medidata.MAuth.Core/MAuthAuthenticator.cs index 9cb1bf7..564d470 100644 --- a/src/Medidata.MAuth.Core/MAuthAuthenticator.cs +++ b/src/Medidata.MAuth.Core/MAuthAuthenticator.cs @@ -43,16 +43,19 @@ public async Task AuthenticateRequest(HttpRequestMessage request) try { logger.LogInformation("Initiating Authentication of the request."); - var version = request.GetAuthHeaderValue().GetVersionFromAuthenticationHeader(); + + var authHeader = request.GetAuthHeaderValue(); + var version = authHeader.GetVersionFromAuthenticationHeader(); + var parsedHeader = authHeader.ParseAuthenticationHeader(); if (options.DisableV1 && version == MAuthVersion.MWS) throw new InvalidVersionException($"Authentication with {version} version is disabled."); - var authenticated = await Authenticate(request, version).ConfigureAwait(false); + var authenticated = await Authenticate(request, version, parsedHeader.Uuid).ConfigureAwait(false); if (!authenticated && version == MAuthVersion.MWSV2 && !options.DisableV1) { // fall back to V1 authentication - authenticated = await Authenticate(request, MAuthVersion.MWS).ConfigureAwait(false); + authenticated = await Authenticate(request, MAuthVersion.MWS, parsedHeader.Uuid).ConfigureAwait(false); logger.LogWarning("Completed successful authentication attempt after fallback to V1"); } return authenticated; @@ -89,10 +92,10 @@ public async Task AuthenticateRequest(HttpRequestMessage request) } } - private async Task Authenticate(HttpRequestMessage request, MAuthVersion version) + private async Task Authenticate(HttpRequestMessage request, MAuthVersion version, Guid signedAppUuid) { - var logMessage = "Mauth-client attempting to authenticate request from app with mauth app uuid" + - $" {options.ApplicationUuid} using version {version}"; + var logMessage = "Mauth-client attempting to authenticate request from app with mauth app uuid " + + $"{signedAppUuid} to app with mauth app uuid {options.ApplicationUuid} using version {version}"; logger.LogInformation(logMessage); var mAuthCore = MAuthCoreFactory.Instantiate(version); diff --git a/src/Medidata.MAuth.Core/MAuthCoreExtensions.cs b/src/Medidata.MAuth.Core/MAuthCoreExtensions.cs index 3a4f484..1031b0d 100644 --- a/src/Medidata.MAuth.Core/MAuthCoreExtensions.cs +++ b/src/Medidata.MAuth.Core/MAuthCoreExtensions.cs @@ -184,17 +184,58 @@ public static byte[] Concat(this byte[][] values) /// EncodedQueryParameter string. public static string BuildEncodedQueryParams(this string queryString) { + if (string.IsNullOrEmpty(queryString)) + return string.Empty; + var encodedQueryStrings = new List(); + var unEscapedQueryStrings = new List(); var queryArray = queryString.Split('&'); - Array.Sort(queryArray, StringComparer.Ordinal); Array.ForEach(queryArray, x => + { + var keyValue = x.Split('='); + var unEscapedKey = Uri.UnescapeDataString(keyValue[0]); + var unEscapedValue = Uri.UnescapeDataString(keyValue[1]); + unEscapedQueryStrings.Add($"{unEscapedKey}={unEscapedValue}"); + }); + var unEscapedQueryArray = unEscapedQueryStrings.ToArray(); + Array.Sort(unEscapedQueryArray, StringComparer.Ordinal); + Array.ForEach(unEscapedQueryArray, x => { var keyValue = x.Split('='); var escapedKey = Uri.EscapeDataString(keyValue[0]); var escapedValue = Uri.EscapeDataString(keyValue[1]); encodedQueryStrings.Add($"{escapedKey}={escapedValue}"); }); - return string.Join("&", encodedQueryStrings); + var result = string.Join("&", encodedQueryStrings); + return result.Replace("%2B","%20"); + } + + /// + /// Normalizes the UriPath + /// + /// + /// Normalized Uri Resource Path + public static string NormalizeUriPath(this string path) + { + if (string.IsNullOrEmpty(path)) + return string.Empty; + + // Normalize percent encoding to uppercase i.e. %cf%80 => %CF%80 + Regex regexHexLowcase = new Regex("%[a-f0-9]{2}", RegexOptions.Compiled); + var match = regexHexLowcase.Match(path); + string normalizedPath = path; + while(match.Success) + { + normalizedPath = normalizedPath.Replace(match.Value, match.Value.ToUpperInvariant()); + match = match.NextMatch(); + } + + // Replacing "//" and "///" into single "/" + normalizedPath = normalizedPath.Replace("///", "/").Replace("//", "/"); + + if (!normalizedPath.EndsWith("/") && (path.EndsWith("/") || path.EndsWith("/.") || path.EndsWith("/.."))) + normalizedPath = $"{normalizedPath}/"; + return normalizedPath; } } } diff --git a/src/Medidata.MAuth.Core/MAuthCoreV2.cs b/src/Medidata.MAuth.Core/MAuthCoreV2.cs index 8444b2c..cc6c92a 100644 --- a/src/Medidata.MAuth.Core/MAuthCoreV2.cs +++ b/src/Medidata.MAuth.Core/MAuthCoreV2.cs @@ -59,7 +59,7 @@ public bool Verify(byte[] signedData, byte[] signature, string publicKey) public async Task GetSignature(HttpRequestMessage request, AuthenticationInfo authInfo) { var encodedHttpVerb = request.Method.Method.ToBytes(); - var encodedResourceUriPath = request.RequestUri.AbsolutePath.ToBytes(); + var encodedResourceUriPath = request.RequestUri.AbsolutePath.NormalizeUriPath().ToBytes(); var encodedAppUUid = authInfo.ApplicationUuid.ToHyphenString().ToBytes(); var requestBody = request.Content != null ? @@ -67,8 +67,11 @@ public async Task GetSignature(HttpRequestMessage request, Authenticatio var requestBodyDigest = requestBody.AsSHA512Hash(); var encodedCurrentSecondsSinceEpoch = authInfo.SignedTime.ToUnixTimeSeconds().ToString().ToBytes(); - var encodedQueryParams = !string.IsNullOrEmpty(request.RequestUri.Query) ? - request.RequestUri.Query.Replace("?", "").BuildEncodedQueryParams().ToBytes() : new byte[] { }; + var queryString = request.RequestUri.Query; + var encodedQueryParams = !string.IsNullOrEmpty(queryString) + ? queryString.Substring(queryString.IndexOf("?") + 1, queryString.Length - 1) + .BuildEncodedQueryParams().ToBytes() + : new byte[] { }; return new byte[][] { diff --git a/tests/Medidata.MAuth.Tests/MAuthCoreExtensionsTests.cs b/tests/Medidata.MAuth.Tests/MAuthCoreExtensionsTests.cs index daeaae1..d6596c2 100644 --- a/tests/Medidata.MAuth.Tests/MAuthCoreExtensionsTests.cs +++ b/tests/Medidata.MAuth.Tests/MAuthCoreExtensionsTests.cs @@ -1,4 +1,5 @@ using Medidata.MAuth.Core; +using System; using Xunit; namespace Medidata.MAuth.Tests @@ -9,7 +10,7 @@ public static class MAuthCoreExtensionsTests public static void BuildEncodedQueryParams_WillEncodeQueryStringWithSpecialCharacters() { var queryString = "key=-_.~!@#$%^*()+{}|:\"'`<>?"; - var expected = "key=-_.~%21%40%23%24%25%5E%2A%28%29%2B%7B%7D%7C%3A%22%27%60%3C%3E%3F"; + var expected = "key=-_.~%21%40%23%24%25%5E%2A%28%29%20%7B%7D%7C%3A%22%27%60%3C%3E%3F"; Assert.Equal(queryString.BuildEncodedQueryParams(), expected); } @@ -35,5 +36,62 @@ public static void BuildEncodedQueryParams_WillHandlesQueryStringWithEmptyValues var queryString = "k=&k=v"; Assert.Equal(queryString.BuildEncodedQueryParams(), queryString); } + + [Fact] + public static void BuildEncodedQueryParams_WithUnescapedTilda() + { + var queryString = "k=%7E"; + var expectedString = "k=~"; + Assert.Equal(expectedString, queryString.BuildEncodedQueryParams()); + } + + [Fact] + public static void BuildEncodedQueryParams_SortAfterUnescaping() + { + var queryString = "k=%7E&k=~&k=%40&k=a"; + var expectedString = "k=%40&k=a&k=~&k=~"; + Assert.Equal(expectedString, queryString.BuildEncodedQueryParams()); + } + + [Fact] + public static void BuildEncodedQueryParams_WithNullQueryString() + { + string queryString = null; + Assert.Empty(queryString.BuildEncodedQueryParams()); + } + + [Fact] + public static void NormalizeUriPath_WithNullPath() + { + string path =null; + Assert.Empty(path.NormalizeUriPath()); + } + + [Fact] + public static void NormalizeUriPath_WithValues() + { + var testcases = CreateUriPathArray(); + for(int i = 0; i <= testcases.GetUpperBound(0); i++) + { + var request = new Uri("http://localhost:2999" + testcases[i, 0]); + Assert.Equal(testcases[i,1], request.AbsolutePath.NormalizeUriPath()); + } + } + + private static string [ , ] CreateUriPathArray() + { + return new string [10, 2] { + { "/example/sample", "/example/sample"}, + {"/example//sample/", "/example/sample/"}, + {"//example///sample/", "/example/sample/"}, + {"/%2a%80", "/%2A%80"}, + { "/example/", "/example/" }, + {"/example/sample/..", "/example/"}, + {"/example/sample/..", "/example/"}, + {"/example/sample/../../../..", "/"}, + {"/example//./.", "/example/"}, + {"/./example/./.", "/example/"} + }; + } } } diff --git a/tests/Medidata.MAuth.Tests/MAuthCoreV2Tests.cs b/tests/Medidata.MAuth.Tests/MAuthCoreV2Tests.cs index 8370fde..1a2656b 100644 --- a/tests/Medidata.MAuth.Tests/MAuthCoreV2Tests.cs +++ b/tests/Medidata.MAuth.Tests/MAuthCoreV2Tests.cs @@ -97,7 +97,7 @@ public static async Task GetSignature_WithRequest_WillReturnTheCorrectSignature( var expectedSignature = new byte[][] { testData.Method.ToBytes(), Constants.NewLine, - testData.Url.AbsolutePath.ToBytes(), Constants.NewLine, + testData.Url.AbsolutePath.NormalizeUriPath().ToBytes(), Constants.NewLine, content.AsSHA512Hash(), Constants.NewLine, testData.ApplicationUuidString.ToBytes(), Constants.NewLine, testData.SignedTimeUnixSeconds.ToString().ToBytes(), Constants.NewLine, diff --git a/version.props b/version.props index 3891445..94418e5 100644 --- a/version.props +++ b/version.props @@ -1,6 +1,6 @@  - 4.0.2 + 4.0.3 From fe32c60b6dffacd1ed537dd26660563c0c39c100 Mon Sep 17 00:00:00 2001 From: Prajon Date: Fri, 10 Jul 2020 11:26:51 -0400 Subject: [PATCH 02/21] Implemented feedbacks --- CHANGELOG.md | 2 +- src/Medidata.MAuth.Core/MAuthCoreExtensions.cs | 9 ++++++--- version.props | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b76829..13b2be4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changes in Medidata.MAuth -## v4.0.3 +## v4.1.0 - **[Core]** Added normalization of Uri AbsolutePath. - **[Core]** Added unescape step in query_string encoding to remove `double encoding`. diff --git a/src/Medidata.MAuth.Core/MAuthCoreExtensions.cs b/src/Medidata.MAuth.Core/MAuthCoreExtensions.cs index 1031b0d..4a0fb13 100644 --- a/src/Medidata.MAuth.Core/MAuthCoreExtensions.cs +++ b/src/Medidata.MAuth.Core/MAuthCoreExtensions.cs @@ -207,7 +207,12 @@ public static string BuildEncodedQueryParams(this string queryString) encodedQueryStrings.Add($"{escapedKey}={escapedValue}"); }); var result = string.Join("&", encodedQueryStrings); - return result.Replace("%2B","%20"); + + // Above encoding converts space as `%20` and `+` as `%2B` + // But space and `+` both needs to be converted as `%20` as per + // reference https://github.com/mdsol/mauth-client-ruby/blob/master/lib/mauth/request_and_response.rb#L113 + // so this convert `%2B` into `%20` to match encodedqueryparams to that of other languages. + return result.Replace("%2B", "%20"); } /// @@ -233,8 +238,6 @@ public static string NormalizeUriPath(this string path) // Replacing "//" and "///" into single "/" normalizedPath = normalizedPath.Replace("///", "/").Replace("//", "/"); - if (!normalizedPath.EndsWith("/") && (path.EndsWith("/") || path.EndsWith("/.") || path.EndsWith("/.."))) - normalizedPath = $"{normalizedPath}/"; return normalizedPath; } } diff --git a/version.props b/version.props index 94418e5..c203214 100644 --- a/version.props +++ b/version.props @@ -1,6 +1,6 @@  - 4.0.3 + 4.1.0 From 29c4055b9ebef70a40748011c0f59d73ea399196 Mon Sep 17 00:00:00 2001 From: Prajon Date: Fri, 10 Jul 2020 14:04:29 -0400 Subject: [PATCH 03/21] Updated the reference link in comment to point to tagged version link --- src/Medidata.MAuth.Core/MAuthCoreExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Medidata.MAuth.Core/MAuthCoreExtensions.cs b/src/Medidata.MAuth.Core/MAuthCoreExtensions.cs index 4a0fb13..3ebd906 100644 --- a/src/Medidata.MAuth.Core/MAuthCoreExtensions.cs +++ b/src/Medidata.MAuth.Core/MAuthCoreExtensions.cs @@ -210,7 +210,7 @@ public static string BuildEncodedQueryParams(this string queryString) // Above encoding converts space as `%20` and `+` as `%2B` // But space and `+` both needs to be converted as `%20` as per - // reference https://github.com/mdsol/mauth-client-ruby/blob/master/lib/mauth/request_and_response.rb#L113 + // reference https://github.com/mdsol/mauth-client-ruby/blob/v6.0.0/lib/mauth/request_and_response.rb#L113 // so this convert `%2B` into `%20` to match encodedqueryparams to that of other languages. return result.Replace("%2B", "%20"); } From 3d0a5fb804b6f3bc84b2f2c51348542ad95b2e6d Mon Sep 17 00:00:00 2001 From: Prajon Date: Fri, 10 Jul 2020 15:42:46 -0400 Subject: [PATCH 04/21] Refactor to use regex.replace --- src/Medidata.MAuth.Core/MAuthCoreExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Medidata.MAuth.Core/MAuthCoreExtensions.cs b/src/Medidata.MAuth.Core/MAuthCoreExtensions.cs index 3ebd906..c1de2a2 100644 --- a/src/Medidata.MAuth.Core/MAuthCoreExtensions.cs +++ b/src/Medidata.MAuth.Core/MAuthCoreExtensions.cs @@ -235,8 +235,8 @@ public static string NormalizeUriPath(this string path) match = match.NextMatch(); } - // Replacing "//" and "///" into single "/" - normalizedPath = normalizedPath.Replace("///", "/").Replace("//", "/"); + // Replaces multiple slashes into single "/" + normalizedPath = Regex.Replace(normalizedPath, "//+", "/"); return normalizedPath; } From a16d0e0bc4d2be4f33b4fa098361114deaba8c37 Mon Sep 17 00:00:00 2001 From: Prajon Date: Mon, 13 Jul 2020 08:04:08 -0400 Subject: [PATCH 05/21] Refactor and nits implemented --- src/Medidata.MAuth.Core/MAuthCoreExtensions.cs | 1 + src/Medidata.MAuth.Core/MAuthCoreV2.cs | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Medidata.MAuth.Core/MAuthCoreExtensions.cs b/src/Medidata.MAuth.Core/MAuthCoreExtensions.cs index c1de2a2..14d89e3 100644 --- a/src/Medidata.MAuth.Core/MAuthCoreExtensions.cs +++ b/src/Medidata.MAuth.Core/MAuthCoreExtensions.cs @@ -198,6 +198,7 @@ public static string BuildEncodedQueryParams(this string queryString) unEscapedQueryStrings.Add($"{unEscapedKey}={unEscapedValue}"); }); var unEscapedQueryArray = unEscapedQueryStrings.ToArray(); + Array.Sort(unEscapedQueryArray, StringComparer.Ordinal); Array.ForEach(unEscapedQueryArray, x => { diff --git a/src/Medidata.MAuth.Core/MAuthCoreV2.cs b/src/Medidata.MAuth.Core/MAuthCoreV2.cs index cc6c92a..bcec82b 100644 --- a/src/Medidata.MAuth.Core/MAuthCoreV2.cs +++ b/src/Medidata.MAuth.Core/MAuthCoreV2.cs @@ -69,8 +69,7 @@ public async Task GetSignature(HttpRequestMessage request, Authenticatio var encodedCurrentSecondsSinceEpoch = authInfo.SignedTime.ToUnixTimeSeconds().ToString().ToBytes(); var queryString = request.RequestUri.Query; var encodedQueryParams = !string.IsNullOrEmpty(queryString) - ? queryString.Substring(queryString.IndexOf("?") + 1, queryString.Length - 1) - .BuildEncodedQueryParams().ToBytes() + ? queryString.Substring(1).BuildEncodedQueryParams().ToBytes() : new byte[] { }; return new byte[][] From 2a6e7fef2f8201f4cc6c08cb00c48ec48e870ce9 Mon Sep 17 00:00:00 2001 From: Prajon Date: Tue, 14 Jul 2020 08:01:19 -0400 Subject: [PATCH 06/21] Use of Theories and converting to parameterized unit test --- .../MAuthCoreExtensionsTests.cs | 37 +++++++------------ 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/tests/Medidata.MAuth.Tests/MAuthCoreExtensionsTests.cs b/tests/Medidata.MAuth.Tests/MAuthCoreExtensionsTests.cs index d6596c2..c70ba9f 100644 --- a/tests/Medidata.MAuth.Tests/MAuthCoreExtensionsTests.cs +++ b/tests/Medidata.MAuth.Tests/MAuthCoreExtensionsTests.cs @@ -67,31 +67,20 @@ public static void NormalizeUriPath_WithNullPath() Assert.Empty(path.NormalizeUriPath()); } - [Fact] - public static void NormalizeUriPath_WithValues() - { - var testcases = CreateUriPathArray(); - for(int i = 0; i <= testcases.GetUpperBound(0); i++) - { - var request = new Uri("http://localhost:2999" + testcases[i, 0]); - Assert.Equal(testcases[i,1], request.AbsolutePath.NormalizeUriPath()); - } - } - - private static string [ , ] CreateUriPathArray() + [Theory] + [InlineData("/example/sample", "/example/sample")] + [InlineData("/example//sample/", "/example/sample/")] + [InlineData("//example///sample/", "/example/sample/")] + [InlineData("/%2a%80", "/%2A%80")] + [InlineData("/example/", "/example/")] + [InlineData("/example/sample/..", "/example/")] + [InlineData("/example/sample/../../../..", "/")] + [InlineData("/example//./.", "/example/")] + [InlineData("/./example/./.", "/example/")] + public static void NormalizeUriPath_WithValues(string input, string expected) { - return new string [10, 2] { - { "/example/sample", "/example/sample"}, - {"/example//sample/", "/example/sample/"}, - {"//example///sample/", "/example/sample/"}, - {"/%2a%80", "/%2A%80"}, - { "/example/", "/example/" }, - {"/example/sample/..", "/example/"}, - {"/example/sample/..", "/example/"}, - {"/example/sample/../../../..", "/"}, - {"/example//./.", "/example/"}, - {"/./example/./.", "/example/"} - }; + var request = new Uri("http://localhost:2999" + input); + Assert.Equal(expected, request.AbsolutePath.NormalizeUriPath()); } } } From a3e46f8b68d86115e3e6689ac02ad83c43d4aeb0 Mon Sep 17 00:00:00 2001 From: Prajon Date: Tue, 14 Jul 2020 10:55:59 -0400 Subject: [PATCH 07/21] Refactored the method to build encoded queryparams and normalize uripath --- .../MAuthCoreExtensions.cs | 39 +++++++++---------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/src/Medidata.MAuth.Core/MAuthCoreExtensions.cs b/src/Medidata.MAuth.Core/MAuthCoreExtensions.cs index 14d89e3..a4ffd31 100644 --- a/src/Medidata.MAuth.Core/MAuthCoreExtensions.cs +++ b/src/Medidata.MAuth.Core/MAuthCoreExtensions.cs @@ -187,33 +187,34 @@ public static string BuildEncodedQueryParams(this string queryString) if (string.IsNullOrEmpty(queryString)) return string.Empty; - var encodedQueryStrings = new List(); - var unEscapedQueryStrings = new List(); var queryArray = queryString.Split('&'); - Array.ForEach(queryArray, x => + + // unescaping + for (int i = 0; i < queryArray.Length; i++) { - var keyValue = x.Split('='); + var keyValue = queryArray.ElementAt(i).Split('='); var unEscapedKey = Uri.UnescapeDataString(keyValue[0]); var unEscapedValue = Uri.UnescapeDataString(keyValue[1]); - unEscapedQueryStrings.Add($"{unEscapedKey}={unEscapedValue}"); - }); - var unEscapedQueryArray = unEscapedQueryStrings.ToArray(); + queryArray[i] = queryArray[i].Replace(queryArray.ElementAt(i), $"{unEscapedKey}={unEscapedValue}"); + } + + // sorting + Array.Sort(queryArray, StringComparer.Ordinal); - Array.Sort(unEscapedQueryArray, StringComparer.Ordinal); - Array.ForEach(unEscapedQueryArray, x => + // escaping + for (int i = 0; i < queryArray.Length; i++) { - var keyValue = x.Split('='); + var keyValue = queryArray.ElementAt(i).Split('='); var escapedKey = Uri.EscapeDataString(keyValue[0]); var escapedValue = Uri.EscapeDataString(keyValue[1]); - encodedQueryStrings.Add($"{escapedKey}={escapedValue}"); - }); - var result = string.Join("&", encodedQueryStrings); + queryArray[i] = queryArray[i].Replace(queryArray.ElementAt(i), $"{escapedKey}={escapedValue}"); + } // Above encoding converts space as `%20` and `+` as `%2B` // But space and `+` both needs to be converted as `%20` as per // reference https://github.com/mdsol/mauth-client-ruby/blob/v6.0.0/lib/mauth/request_and_response.rb#L113 // so this convert `%2B` into `%20` to match encodedqueryparams to that of other languages. - return result.Replace("%2B", "%20"); + return string.Join("&", queryArray).Replace("%2B", "%20"); } /// @@ -227,19 +228,15 @@ public static string NormalizeUriPath(this string path) return string.Empty; // Normalize percent encoding to uppercase i.e. %cf%80 => %CF%80 - Regex regexHexLowcase = new Regex("%[a-f0-9]{2}", RegexOptions.Compiled); - var match = regexHexLowcase.Match(path); - string normalizedPath = path; + var match = new Regex("%[a-f0-9]{2}").Match(path); while(match.Success) { - normalizedPath = normalizedPath.Replace(match.Value, match.Value.ToUpperInvariant()); + path = path.Replace(match.Value, match.Value.ToUpperInvariant()); match = match.NextMatch(); } // Replaces multiple slashes into single "/" - normalizedPath = Regex.Replace(normalizedPath, "//+", "/"); - - return normalizedPath; + return new Regex("//+").Replace(path, "/"); } } } From e995a68edc19c376bbf5909afc1c64b716158371 Mon Sep 17 00:00:00 2001 From: Prajon Date: Wed, 15 Jul 2020 14:12:19 -0400 Subject: [PATCH 08/21] Refactored `BuildEncodedQueryParams` method --- src/Medidata.MAuth.Core/MAuthCoreExtensions.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Medidata.MAuth.Core/MAuthCoreExtensions.cs b/src/Medidata.MAuth.Core/MAuthCoreExtensions.cs index a4ffd31..74a2b14 100644 --- a/src/Medidata.MAuth.Core/MAuthCoreExtensions.cs +++ b/src/Medidata.MAuth.Core/MAuthCoreExtensions.cs @@ -192,10 +192,10 @@ public static string BuildEncodedQueryParams(this string queryString) // unescaping for (int i = 0; i < queryArray.Length; i++) { - var keyValue = queryArray.ElementAt(i).Split('='); + var keyValue = queryArray[i].Split('='); var unEscapedKey = Uri.UnescapeDataString(keyValue[0]); var unEscapedValue = Uri.UnescapeDataString(keyValue[1]); - queryArray[i] = queryArray[i].Replace(queryArray.ElementAt(i), $"{unEscapedKey}={unEscapedValue}"); + queryArray[i] = $"{unEscapedKey}={unEscapedValue}"; } // sorting @@ -204,10 +204,10 @@ public static string BuildEncodedQueryParams(this string queryString) // escaping for (int i = 0; i < queryArray.Length; i++) { - var keyValue = queryArray.ElementAt(i).Split('='); + var keyValue = queryArray[i].Split('='); var escapedKey = Uri.EscapeDataString(keyValue[0]); var escapedValue = Uri.EscapeDataString(keyValue[1]); - queryArray[i] = queryArray[i].Replace(queryArray.ElementAt(i), $"{escapedKey}={escapedValue}"); + queryArray[i] = $"{escapedKey}={escapedValue}"; } // Above encoding converts space as `%20` and `+` as `%2B` From 438fdc8e1cc5bab8870e2ccb5eae96058a87117b Mon Sep 17 00:00:00 2001 From: Prajon Date: Wed, 15 Jul 2020 15:14:33 -0400 Subject: [PATCH 09/21] Refactored `NormalizedUriPath()` method --- src/Medidata.MAuth.Core/Constants.cs | 4 ++++ src/Medidata.MAuth.Core/MAuthCoreExtensions.cs | 10 +++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Medidata.MAuth.Core/Constants.cs b/src/Medidata.MAuth.Core/Constants.cs index 264723f..29198ab 100644 --- a/src/Medidata.MAuth.Core/Constants.cs +++ b/src/Medidata.MAuth.Core/Constants.cs @@ -45,5 +45,9 @@ internal static class Constants public static readonly string MAuthTimeHeaderKeyV2 = "MCC-Time"; public static readonly string MAuthTokenRequestPath = "/mauth/v1/security_tokens/"; + + public static readonly Regex LowerCaseHexPattern = new Regex("%[a-f0-9]{2}", RegexOptions.Compiled); + + public static readonly Regex SlashPattern = new Regex("//+", RegexOptions.Compiled); } } diff --git a/src/Medidata.MAuth.Core/MAuthCoreExtensions.cs b/src/Medidata.MAuth.Core/MAuthCoreExtensions.cs index 74a2b14..209e0bb 100644 --- a/src/Medidata.MAuth.Core/MAuthCoreExtensions.cs +++ b/src/Medidata.MAuth.Core/MAuthCoreExtensions.cs @@ -228,15 +228,15 @@ public static string NormalizeUriPath(this string path) return string.Empty; // Normalize percent encoding to uppercase i.e. %cf%80 => %CF%80 - var match = new Regex("%[a-f0-9]{2}").Match(path); - while(match.Success) + var matches = Constants.LowerCaseHexPattern.Matches(path); + var normalizedPath = new StringBuilder(path); + foreach(var item in matches) { - path = path.Replace(match.Value, match.Value.ToUpperInvariant()); - match = match.NextMatch(); + normalizedPath.Replace(item.ToString(), item.ToString().ToUpper()); } // Replaces multiple slashes into single "/" - return new Regex("//+").Replace(path, "/"); + return Constants.SlashPattern.Replace(normalizedPath.ToString(), "/"); } } } From 9fa55ecddcffaa6f7132103e3a5f5e0324b789a1 Mon Sep 17 00:00:00 2001 From: Prajon Date: Fri, 17 Jul 2020 13:39:14 -0400 Subject: [PATCH 10/21] Replaced boolean "DisableV1" option in Signing with "SigningOptions" and change the default to v2 --- CHANGELOG.md | 1 + README.md | 15 ++-- .../MAuthSigningHandler.cs | 40 ++++++++-- .../Options/MAuthSigningOptions.cs | 5 +- .../MAuthSigningHandlerTests.cs | 75 ++++++++++++++++--- 5 files changed, 109 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13b2be4..6fe201d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## v4.1.0 - **[Core]** Added normalization of Uri AbsolutePath. - **[Core]** Added unescape step in query_string encoding to remove `double encoding`. + - **[Core]** Replace `DisableV1`option with `SigningOptions` option and change the default to `v2` only. ## v4.0.2 - **[AspNetCore]** Update aspnetcore version to aspnetcore2.1 LTS. diff --git a/README.md b/README.md index 182bd2d..08b1560 100644 --- a/README.md +++ b/README.md @@ -114,9 +114,8 @@ public async Task SignAndSendRequest(HttpRequestMessage req // The following can be either a path to the key file or the contents of the file itself PrivateKey = "ClientPrivateKey.pem", - // when ready to disable authentication of V1 protocol else default is false - // signs with both V1 and V2. - DisableV1 = true + // comma-separated values of signing protocols, if not provided defaults to v2 + SigningOptions = "v1,v2" }); using (var client = new HttpClient(signingHandler)) @@ -125,9 +124,11 @@ public async Task SignAndSendRequest(HttpRequestMessage req } } ``` -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`. +The `SigningOptions` parameter can be used to specify which protocol version to sign outgoing requests. Like as: +`SigningOptions ="v1"`: signs with v1 protocol only. +`SigningOptions ="v1, v2"` : signs with both v1 and v2 protocol. +If not supplied, it sign by `v2` by default. + 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 @@ -139,7 +140,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. | -| **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`. | +| **SigningOptions** | (optional) Comma-separated protocol versions to sign requests. If not supplied, defaults to `v2`. ### Authenticating Incoming Requests with the OWIN and ASP.NET Core Middlewares diff --git a/src/Medidata.MAuth.Core/MAuthSigningHandler.cs b/src/Medidata.MAuth.Core/MAuthSigningHandler.cs index dc679ee..ba3f831 100644 --- a/src/Medidata.MAuth.Core/MAuthSigningHandler.cs +++ b/src/Medidata.MAuth.Core/MAuthSigningHandler.cs @@ -1,7 +1,10 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using Medidata.MAuth.Core.Exceptions; using Medidata.MAuth.Core.Models; namespace Medidata.MAuth.Core @@ -55,20 +58,41 @@ protected override async Task SendAsync( if (InnerHandler == null) InnerHandler = new HttpClientHandler(); - if (options.DisableV1 == false) // default + var signingVersions = GetSigningVersions(options.SigningOptions); + foreach(var version in signingVersions) { - // Add headers for V1 protocol as well - var mAuthCoreV1 = MAuthCoreFactory.Instantiate(MAuthVersion.MWS); - request = await mAuthCoreV1.Sign(request, options).ConfigureAwait(false); + var mAuthCore = MAuthCoreFactory.Instantiate(version); + request = await mAuthCore.Sign(request, options).ConfigureAwait(false); } - // Add headers for V2 protocol - mAuthCore = MAuthCoreFactory.Instantiate(MAuthVersion.MWSV2); - request = await mAuthCore.Sign(request, options).ConfigureAwait(false); - return await base .SendAsync(request, cancellationToken) .ConfigureAwait(continueOnCapturedContext: false); } + + private List GetSigningVersions(string signingOptions) + { + var signVersions = new List(); + if (string.IsNullOrEmpty(signingOptions)) + return new List() { MAuthVersion.MWSV2 }; + + var signingArray = signingOptions.ToLower().Split(','); + foreach(var item in signingArray) + { + switch (item.Trim()) + { + case "v1": + signVersions.Add(MAuthVersion.MWS); + break; + case "v2": + signVersions.Add(MAuthVersion.MWSV2); + break; + } + } + if(signingArray.Any() && !signVersions.Any()) + throw new InvalidVersionException($"Signing with {signingOptions} version is not allowed."); + + return signVersions; + } } } diff --git a/src/Medidata.MAuth.Core/Options/MAuthSigningOptions.cs b/src/Medidata.MAuth.Core/Options/MAuthSigningOptions.cs index 82081d6..ee4fa17 100644 --- a/src/Medidata.MAuth.Core/Options/MAuthSigningOptions.cs +++ b/src/Medidata.MAuth.Core/Options/MAuthSigningOptions.cs @@ -1,5 +1,4 @@ using System; -using Medidata.MAuth.Core.Models; namespace Medidata.MAuth.Core { @@ -23,8 +22,8 @@ public class MAuthSigningOptions internal DateTimeOffset? SignedTime { get; set; } /// - /// Determines the boolean value if V1 option of signing should be disabled or not with default value of false. + /// Comma-separated protocol versions to sign requests, if not provided defaults to "v2". /// - public bool DisableV1 { get; set; } = false; + public string SigningOptions { get; set; } } } diff --git a/tests/Medidata.MAuth.Tests/MAuthSigningHandlerTests.cs b/tests/Medidata.MAuth.Tests/MAuthSigningHandlerTests.cs index 7d20d21..8046375 100644 --- a/tests/Medidata.MAuth.Tests/MAuthSigningHandlerTests.cs +++ b/tests/Medidata.MAuth.Tests/MAuthSigningHandlerTests.cs @@ -1,6 +1,7 @@ using System.Net.Http; using System.Threading.Tasks; using Medidata.MAuth.Core; +using Medidata.MAuth.Core.Exceptions; using Medidata.MAuth.Core.Models; using Medidata.MAuth.Tests.Infrastructure; using Xunit; @@ -14,25 +15,26 @@ public static class MAuthSigningHandlerTests [InlineData("DELETE")] [InlineData("POST")] [InlineData("PUT")] - public static async Task SendAsync_WithDefault_WillSignProperly_BothMWSAndMWSV2(string method) + public static async Task SendAsync_WithNoSigningOptions_WillSignWithOnlyMWSV2(string method) { // Arrange var testData = await method.FromResource(); var actual = new AssertSigningHandler(); + var version = MAuthVersion.MWSV2; var signingHandler = new MAuthSigningHandler(TestExtensions.ClientOptions(testData.SignedTime), actual); // Act using (var client = new HttpClient(signingHandler)) { - await client.SendAsync(testData.ToDefaultHttpRequestMessage()); + await client.SendAsync(testData.ToHttpRequestMessage(version)); } // Assert - Assert.Equal(testData.MAuthHeader, actual.MAuthHeader); - Assert.Equal(testData.SignedTime, long.Parse(actual.MAuthTimeHeader).FromUnixTimeSeconds()); + Assert.Null(actual.MAuthHeader); + Assert.Null(actual.MAuthTimeHeader); Assert.Equal(testData.MAuthHeaderV2, actual.MAuthHeaderV2); - Assert.Equal(testData.SignedTime, long.Parse(actual.MAuthTimeHeader).FromUnixTimeSeconds()); + Assert.Equal(testData.SignedTime, long.Parse(actual.MAuthTimeHeaderV2).FromUnixTimeSeconds()); } [Theory] @@ -40,17 +42,16 @@ public static async Task SendAsync_WithDefault_WillSignProperly_BothMWSAndMWSV2( [InlineData("DELETE")] [InlineData("POST")] [InlineData("PUT")] - public static async Task SendAsync_WithDisableV1_WillSignProperlyWithMWSV2(string method) + public static async Task SendAsync_WithSigningOptionsV1_WillSignWithMWS(string method) { // Arrange var testData = await method.FromResourceV2(); var actual = new AssertSigningHandler(); - var version = MAuthVersion.MWSV2; var clientOptions = TestExtensions.ClientOptions(testData.SignedTime); - clientOptions.DisableV1 = true; + clientOptions.SigningOptions = "v1"; + var version = MAuthVersion.MWS; var signingHandler = new MAuthSigningHandler(clientOptions, actual); - // Act using (var client = new HttpClient(signingHandler)) { @@ -58,8 +59,64 @@ public static async Task SendAsync_WithDisableV1_WillSignProperlyWithMWSV2(strin } // Assert + Assert.Equal(testData.MAuthHeader, actual.MAuthHeader); + Assert.Equal(testData.SignedTime, long.Parse(actual.MAuthTimeHeader).FromUnixTimeSeconds()); + + Assert.Null(actual.MAuthHeaderV2); + Assert.Null(actual.MAuthTimeHeaderV2); + } + + [Theory] + [InlineData("GET")] + [InlineData("DELETE")] + [InlineData("POST")] + [InlineData("PUT")] + public static async Task SendAsync_WithSigningOptionsV1AndV2_WillSignWithBothMWSAndMWSV2(string method) + { + // Arrange + var testData = await method.FromResourceV2(); + var actual = new AssertSigningHandler(); + var clientOptions = TestExtensions.ClientOptions(testData.SignedTime); + clientOptions.SigningOptions = "v1,v2"; + var signingHandler = new MAuthSigningHandler(clientOptions, actual); + + // Act + using (var client = new HttpClient(signingHandler)) + { + await client.SendAsync(testData.ToDefaultHttpRequestMessage()); + } + + // Assert + Assert.Equal(testData.MAuthHeader, actual.MAuthHeader); + Assert.Equal(testData.SignedTime, long.Parse(actual.MAuthTimeHeader).FromUnixTimeSeconds()); + Assert.Equal(testData.MAuthHeaderV2, actual.MAuthHeaderV2); Assert.Equal(testData.SignedTime, long.Parse(actual.MAuthTimeHeaderV2).FromUnixTimeSeconds()); } + + [Theory] + [InlineData("GET")] + [InlineData("DELETE")] + [InlineData("POST")] + [InlineData("PUT")] + public static async Task SendAsync_WithImproperSigningOptions_WillThrowException(string method) + { + // Arrange + var testData = await method.FromResourceV2(); + var actual = new AssertSigningHandler(); + var clientOptions = TestExtensions.ClientOptions(testData.SignedTime); + clientOptions.SigningOptions = "v12"; + var signingHandler = new MAuthSigningHandler(clientOptions, actual); + + // Act + // Assert + using (var client = new HttpClient(signingHandler)) + { + var exception = await Assert.ThrowsAsync(() + => client.SendAsync(testData.ToDefaultHttpRequestMessage())); + Assert.Equal(exception.Message, + $"Signing with {clientOptions.SigningOptions} version is not allowed."); + } + } } } From 75b39eb8f7a2ad2e9d84b9810d0e4a0291d199b5 Mon Sep 17 00:00:00 2001 From: Prajon Date: Mon, 20 Jul 2020 09:58:16 -0400 Subject: [PATCH 11/21] Feedback implementation --- src/Medidata.MAuth.Core/MAuthSigningHandler.cs | 7 ++++--- src/Medidata.MAuth.Core/Options/MAuthSigningOptions.cs | 2 +- tests/Medidata.MAuth.Tests/MAuthSigningHandlerTests.cs | 8 ++++---- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/Medidata.MAuth.Core/MAuthSigningHandler.cs b/src/Medidata.MAuth.Core/MAuthSigningHandler.cs index ba3f831..954c5e5 100644 --- a/src/Medidata.MAuth.Core/MAuthSigningHandler.cs +++ b/src/Medidata.MAuth.Core/MAuthSigningHandler.cs @@ -58,7 +58,7 @@ protected override async Task SendAsync( if (InnerHandler == null) InnerHandler = new HttpClientHandler(); - var signingVersions = GetSigningVersions(options.SigningOptions); + var signingVersions = GetSigningVersions(options.SignVersions); foreach(var version in signingVersions) { var mAuthCore = MAuthCoreFactory.Instantiate(version); @@ -70,12 +70,12 @@ protected override async Task SendAsync( .ConfigureAwait(continueOnCapturedContext: false); } - private List GetSigningVersions(string signingOptions) + private IEnumerable GetSigningVersions(string signingOptions) { - var signVersions = new List(); if (string.IsNullOrEmpty(signingOptions)) return new List() { MAuthVersion.MWSV2 }; + var signVersions = new List(); var signingArray = signingOptions.ToLower().Split(','); foreach(var item in signingArray) { @@ -89,6 +89,7 @@ private List GetSigningVersions(string signingOptions) break; } } + if(signingArray.Any() && !signVersions.Any()) throw new InvalidVersionException($"Signing with {signingOptions} version is not allowed."); diff --git a/src/Medidata.MAuth.Core/Options/MAuthSigningOptions.cs b/src/Medidata.MAuth.Core/Options/MAuthSigningOptions.cs index ee4fa17..3072366 100644 --- a/src/Medidata.MAuth.Core/Options/MAuthSigningOptions.cs +++ b/src/Medidata.MAuth.Core/Options/MAuthSigningOptions.cs @@ -24,6 +24,6 @@ public class MAuthSigningOptions /// /// Comma-separated protocol versions to sign requests, if not provided defaults to "v2". /// - public string SigningOptions { get; set; } + public string SignVersions { get; set; } } } diff --git a/tests/Medidata.MAuth.Tests/MAuthSigningHandlerTests.cs b/tests/Medidata.MAuth.Tests/MAuthSigningHandlerTests.cs index 8046375..7122f25 100644 --- a/tests/Medidata.MAuth.Tests/MAuthSigningHandlerTests.cs +++ b/tests/Medidata.MAuth.Tests/MAuthSigningHandlerTests.cs @@ -48,7 +48,7 @@ public static async Task SendAsync_WithSigningOptionsV1_WillSignWithMWS(string m var testData = await method.FromResourceV2(); var actual = new AssertSigningHandler(); var clientOptions = TestExtensions.ClientOptions(testData.SignedTime); - clientOptions.SigningOptions = "v1"; + clientOptions.SignVersions = "v1"; var version = MAuthVersion.MWS; var signingHandler = new MAuthSigningHandler(clientOptions, actual); @@ -77,7 +77,7 @@ public static async Task SendAsync_WithSigningOptionsV1AndV2_WillSignWithBothMWS var testData = await method.FromResourceV2(); var actual = new AssertSigningHandler(); var clientOptions = TestExtensions.ClientOptions(testData.SignedTime); - clientOptions.SigningOptions = "v1,v2"; + clientOptions.SignVersions = "v1,v2"; var signingHandler = new MAuthSigningHandler(clientOptions, actual); // Act @@ -105,7 +105,7 @@ public static async Task SendAsync_WithImproperSigningOptions_WillThrowException var testData = await method.FromResourceV2(); var actual = new AssertSigningHandler(); var clientOptions = TestExtensions.ClientOptions(testData.SignedTime); - clientOptions.SigningOptions = "v12"; + clientOptions.SignVersions = "v12"; var signingHandler = new MAuthSigningHandler(clientOptions, actual); // Act @@ -115,7 +115,7 @@ public static async Task SendAsync_WithImproperSigningOptions_WillThrowException var exception = await Assert.ThrowsAsync(() => client.SendAsync(testData.ToDefaultHttpRequestMessage())); Assert.Equal(exception.Message, - $"Signing with {clientOptions.SigningOptions} version is not allowed."); + $"Signing with {clientOptions.SignVersions} version is not allowed."); } } } From 2342675ee665111de8befdacf81ec7aa646f2e31 Mon Sep 17 00:00:00 2001 From: Prajon Date: Tue, 21 Jul 2020 09:10:14 -0400 Subject: [PATCH 12/21] Updated `SigningOptions` to `SignVersions` in readme and changelog and updated method name and variable name --- CHANGELOG.md | 2 +- README.md | 10 ++++----- .../MAuthSigningHandler.cs | 22 +++++++++---------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fe201d..f6dffd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ## v4.1.0 - **[Core]** Added normalization of Uri AbsolutePath. - **[Core]** Added unescape step in query_string encoding to remove `double encoding`. - - **[Core]** Replace `DisableV1`option with `SigningOptions` option and change the default to `v2` only. + - **[Core]** Replace `DisableV1`option with `SignVersions` option and change the default to `v2` only. ## v4.0.2 - **[AspNetCore]** Update aspnetcore version to aspnetcore2.1 LTS. diff --git a/README.md b/README.md index 08b1560..5f161af 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ public async Task SignAndSendRequest(HttpRequestMessage req PrivateKey = "ClientPrivateKey.pem", // comma-separated values of signing protocols, if not provided defaults to v2 - SigningOptions = "v1,v2" + SignVersions = "v1,v2" }); using (var client = new HttpClient(signingHandler)) @@ -124,9 +124,9 @@ public async Task SignAndSendRequest(HttpRequestMessage req } } ``` -The `SigningOptions` parameter can be used to specify which protocol version to sign outgoing requests. Like as: -`SigningOptions ="v1"`: signs with v1 protocol only. -`SigningOptions ="v1, v2"` : signs with both v1 and v2 protocol. +The `SignVersions` parameter can be used to specify which protocol version to sign outgoing requests. Like as: +`SignVersions ="v1"`: signs with v1 protocol only. +`SignVersions ="v1, v2"` : signs with both v1 and v2 protocol. If not supplied, it sign by `v2` by default. Signing with V2 protocol supports query string. @@ -140,7 +140,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. | -| **SigningOptions** | (optional) Comma-separated protocol versions to sign requests. If not supplied, defaults to `v2`. +| **SignVersions** | (optional) Comma-separated protocol versions to sign requests. If not supplied, defaults to `v2`. ### Authenticating Incoming Requests with the OWIN and ASP.NET Core Middlewares diff --git a/src/Medidata.MAuth.Core/MAuthSigningHandler.cs b/src/Medidata.MAuth.Core/MAuthSigningHandler.cs index 954c5e5..cb56abe 100644 --- a/src/Medidata.MAuth.Core/MAuthSigningHandler.cs +++ b/src/Medidata.MAuth.Core/MAuthSigningHandler.cs @@ -58,8 +58,8 @@ protected override async Task SendAsync( if (InnerHandler == null) InnerHandler = new HttpClientHandler(); - var signingVersions = GetSigningVersions(options.SignVersions); - foreach(var version in signingVersions) + var mauthVersions = GetMAuthSigningVersions(options.SignVersions); + foreach(var version in mauthVersions) { var mAuthCore = MAuthCoreFactory.Instantiate(version); request = await mAuthCore.Sign(request, options).ConfigureAwait(false); @@ -70,30 +70,30 @@ protected override async Task SendAsync( .ConfigureAwait(continueOnCapturedContext: false); } - private IEnumerable GetSigningVersions(string signingOptions) + private IEnumerable GetMAuthSigningVersions(string signingVersions) { - if (string.IsNullOrEmpty(signingOptions)) + if (string.IsNullOrEmpty(signingVersions)) return new List() { MAuthVersion.MWSV2 }; - var signVersions = new List(); - var signingArray = signingOptions.ToLower().Split(','); + var mauthVersions = new List(); + var signingArray = signingVersions.ToLower().Split(','); foreach(var item in signingArray) { switch (item.Trim()) { case "v1": - signVersions.Add(MAuthVersion.MWS); + mauthVersions.Add(MAuthVersion.MWS); break; case "v2": - signVersions.Add(MAuthVersion.MWSV2); + mauthVersions.Add(MAuthVersion.MWSV2); break; } } - if(signingArray.Any() && !signVersions.Any()) - throw new InvalidVersionException($"Signing with {signingOptions} version is not allowed."); + if(signingArray.Any() && !mauthVersions.Any()) + throw new InvalidVersionException($"Signing with {signingVersions} version is not allowed."); - return signVersions; + return mauthVersions; } } } From 868e891ae175d258138cfd7ee65ad72edb055960 Mon Sep 17 00:00:00 2001 From: Prajon Date: Tue, 21 Jul 2020 18:32:01 -0400 Subject: [PATCH 13/21] Fix spacings, bump major version, updated changelog and updated readme --- CHANGELOG.md | 2 +- README.md | 11 ++++++++--- src/Medidata.MAuth.Core/MAuthSigningHandler.cs | 5 +++-- version.props | 2 +- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6dffd7..4b38338 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changes in Medidata.MAuth -## v4.1.0 +## v5.0.0 - **[Core]** Added normalization of Uri AbsolutePath. - **[Core]** Added unescape step in query_string encoding to remove `double encoding`. - **[Core]** Replace `DisableV1`option with `SignVersions` option and change the default to `v2` only. diff --git a/README.md b/README.md index 5f161af..993eb32 100644 --- a/README.md +++ b/README.md @@ -124,9 +124,9 @@ public async Task SignAndSendRequest(HttpRequestMessage req } } ``` -The `SignVersions` parameter can be used to specify which protocol version to sign outgoing requests. Like as: -`SignVersions ="v1"`: signs with v1 protocol only. -`SignVersions ="v1, v2"` : signs with both v1 and v2 protocol. +The `SignVersions` parameter can be used to specify which protocol version to sign outgoing requests. Like as: +`SignVersions ="v1"`: signs with v1 protocol only. +`SignVersions ="v1, v2"` : signs with both v1 and v2 protocol. If not supplied, it sign by `v2` by default. Signing with V2 protocol supports query string. @@ -331,6 +331,11 @@ On the .NET Framework side (WebAPI, Owin, Core) we are using the latest version [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 5.0.0 version? +In this version we have removed the property `DisableV1` from `MAuthSigningOptions`. Instead, we have added new option as +`SignVersions` in `MAuthSigningOptions` which takes comma-separated values of "v1" and "v2" protocol. If this option is not +provided, then it will sign in by "v2" protocol as default. + ##### 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 diff --git a/src/Medidata.MAuth.Core/MAuthSigningHandler.cs b/src/Medidata.MAuth.Core/MAuthSigningHandler.cs index cb56abe..c7a6031 100644 --- a/src/Medidata.MAuth.Core/MAuthSigningHandler.cs +++ b/src/Medidata.MAuth.Core/MAuthSigningHandler.cs @@ -77,7 +77,8 @@ private IEnumerable GetMAuthSigningVersions(string signingVersions var mauthVersions = new List(); var signingArray = signingVersions.ToLower().Split(','); - foreach(var item in signingArray) + + foreach (var item in signingArray) { switch (item.Trim()) { @@ -90,7 +91,7 @@ private IEnumerable GetMAuthSigningVersions(string signingVersions } } - if(signingArray.Any() && !mauthVersions.Any()) + if (signingArray.Any() && !mauthVersions.Any()) throw new InvalidVersionException($"Signing with {signingVersions} version is not allowed."); return mauthVersions; diff --git a/version.props b/version.props index c203214..4db86b0 100644 --- a/version.props +++ b/version.props @@ -1,6 +1,6 @@  - 4.1.0 + 5.0.0 From de705db8c41b80c5f8c0d83ca4817486072ae669 Mon Sep 17 00:00:00 2001 From: Prajon Date: Thu, 23 Jul 2020 11:21:39 -0400 Subject: [PATCH 14/21] Updated `SignVersions` to be enumeration values of protocol versions instead of comma-separate values --- CHANGELOG.md | 2 +- README.md | 18 ++++----- .../MAuthSigningHandler.cs | 35 ++--------------- .../Options/MAuthSigningOptions.cs | 8 ++-- .../MAuthSigningHandlerTests.cs | 39 ++++--------------- 5 files changed, 25 insertions(+), 77 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b38338..0270c30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ## v5.0.0 - **[Core]** Added normalization of Uri AbsolutePath. - **[Core]** Added unescape step in query_string encoding to remove `double encoding`. - - **[Core]** Replace `DisableV1`option with `SignVersions` option and change the default to `v2` only. + - **[Core]** Replace `DisableV1`option with `SignVersions` option and change the default signing to `MAuthVersion.MWSV2` only. ## v4.0.2 - **[AspNetCore]** Update aspnetcore version to aspnetcore2.1 LTS. diff --git a/README.md b/README.md index 993eb32..6c1fd16 100644 --- a/README.md +++ b/README.md @@ -114,8 +114,8 @@ public async Task SignAndSendRequest(HttpRequestMessage req // The following can be either a path to the key file or the contents of the file itself PrivateKey = "ClientPrivateKey.pem", - // comma-separated values of signing protocols, if not provided defaults to v2 - SignVersions = "v1,v2" + // Enumerations of signing protocols, if not provided defaults to `MAuthVersion.MWSV2`for sign-in. + SignVersions = new List { MAuthVersion.MWS, MAuthVersion.MWSV2 } }); using (var client = new HttpClient(signingHandler)) @@ -125,11 +125,11 @@ public async Task SignAndSendRequest(HttpRequestMessage req } ``` The `SignVersions` parameter can be used to specify which protocol version to sign outgoing requests. Like as: -`SignVersions ="v1"`: signs with v1 protocol only. -`SignVersions ="v1, v2"` : signs with both v1 and v2 protocol. -If not supplied, it sign by `v2` by default. +`SignVersions =new List { MAuthVersion.MWS }`: signs with `MWS` protocol only. +`SignVersions =new List { MAuthVersion.MWS, MAuthVersion.MWSV2 }` : signs with both `MWS` and `MWSV2` protocol. +If not supplied, it sign by `MWSV2` protocol by default. -Signing with V2 protocol supports query string. +Signing with `MWSV2` 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. @@ -140,7 +140,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. | -| **SignVersions** | (optional) Comma-separated protocol versions to sign requests. If not supplied, defaults to `v2`. +| **SignVersions** | (optional) Enumerations of MAuth protocol versions to sign requests. If not supplied, defaults to `MWSV2`. ### Authenticating Incoming Requests with the OWIN and ASP.NET Core Middlewares @@ -333,8 +333,8 @@ the portable fork of the [BouncyCastle](https://github.com/onovotny/BouncyCastle ##### What are the major changes in the 5.0.0 version? In this version we have removed the property `DisableV1` from `MAuthSigningOptions`. Instead, we have added new option as -`SignVersions` in `MAuthSigningOptions` which takes comma-separated values of "v1" and "v2" protocol. If this option is not -provided, then it will sign in by "v2" protocol as default. +`SignVersions` in `MAuthSigningOptions` which takes enumeration values of MAuth protcol versions `MWS` and/ or `MWSV2` protocol. +If this option is not provided, then it will sign in by `MWSV2` protocol as default. ##### What are the major changes in the 4.0.0 version? diff --git a/src/Medidata.MAuth.Core/MAuthSigningHandler.cs b/src/Medidata.MAuth.Core/MAuthSigningHandler.cs index c7a6031..08dae45 100644 --- a/src/Medidata.MAuth.Core/MAuthSigningHandler.cs +++ b/src/Medidata.MAuth.Core/MAuthSigningHandler.cs @@ -1,10 +1,8 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; -using Medidata.MAuth.Core.Exceptions; using Medidata.MAuth.Core.Models; namespace Medidata.MAuth.Core @@ -16,7 +14,6 @@ namespace Medidata.MAuth.Core public class MAuthSigningHandler: DelegatingHandler { private readonly MAuthSigningOptions options; - private IMAuthCore mAuthCore; /// Gets the Uuid of the client application. public Guid ClientAppUuid => options.ApplicationUuid; @@ -29,6 +26,7 @@ public class MAuthSigningHandler: DelegatingHandler public MAuthSigningHandler(MAuthSigningOptions options) { this.options = options; + this.options.SignVersions = options.SignVersions ?? new List { MAuthVersion.MWSV2 }; } /// @@ -42,6 +40,7 @@ public MAuthSigningHandler(MAuthSigningOptions options) public MAuthSigningHandler(MAuthSigningOptions options, HttpMessageHandler innerHandler): base(innerHandler) { this.options = options; + this.options.SignVersions = options.SignVersions ?? new List { MAuthVersion.MWSV2 }; } /// @@ -58,8 +57,7 @@ protected override async Task SendAsync( if (InnerHandler == null) InnerHandler = new HttpClientHandler(); - var mauthVersions = GetMAuthSigningVersions(options.SignVersions); - foreach(var version in mauthVersions) + foreach (var version in options.SignVersions) { var mAuthCore = MAuthCoreFactory.Instantiate(version); request = await mAuthCore.Sign(request, options).ConfigureAwait(false); @@ -69,32 +67,5 @@ protected override async Task SendAsync( .SendAsync(request, cancellationToken) .ConfigureAwait(continueOnCapturedContext: false); } - - private IEnumerable GetMAuthSigningVersions(string signingVersions) - { - if (string.IsNullOrEmpty(signingVersions)) - return new List() { MAuthVersion.MWSV2 }; - - var mauthVersions = new List(); - var signingArray = signingVersions.ToLower().Split(','); - - foreach (var item in signingArray) - { - switch (item.Trim()) - { - case "v1": - mauthVersions.Add(MAuthVersion.MWS); - break; - case "v2": - mauthVersions.Add(MAuthVersion.MWSV2); - break; - } - } - - if (signingArray.Any() && !mauthVersions.Any()) - throw new InvalidVersionException($"Signing with {signingVersions} version is not allowed."); - - return mauthVersions; - } } } diff --git a/src/Medidata.MAuth.Core/Options/MAuthSigningOptions.cs b/src/Medidata.MAuth.Core/Options/MAuthSigningOptions.cs index 3072366..7a9dcf7 100644 --- a/src/Medidata.MAuth.Core/Options/MAuthSigningOptions.cs +++ b/src/Medidata.MAuth.Core/Options/MAuthSigningOptions.cs @@ -1,4 +1,6 @@ -using System; +using Medidata.MAuth.Core.Models; +using System; +using System.Collections.Generic; namespace Medidata.MAuth.Core { @@ -22,8 +24,8 @@ public class MAuthSigningOptions internal DateTimeOffset? SignedTime { get; set; } /// - /// Comma-separated protocol versions to sign requests, if not provided defaults to "v2". + /// Enumeration of MAuth protocol versions to sign requests, if not provided defaults to "v2". /// - public string SignVersions { get; set; } + public IEnumerable SignVersions { get; set; } } } diff --git a/tests/Medidata.MAuth.Tests/MAuthSigningHandlerTests.cs b/tests/Medidata.MAuth.Tests/MAuthSigningHandlerTests.cs index 7122f25..33666fe 100644 --- a/tests/Medidata.MAuth.Tests/MAuthSigningHandlerTests.cs +++ b/tests/Medidata.MAuth.Tests/MAuthSigningHandlerTests.cs @@ -1,7 +1,7 @@ -using System.Net.Http; +using System.Collections.Generic; +using System.Net.Http; using System.Threading.Tasks; using Medidata.MAuth.Core; -using Medidata.MAuth.Core.Exceptions; using Medidata.MAuth.Core.Models; using Medidata.MAuth.Tests.Infrastructure; using Xunit; @@ -15,7 +15,7 @@ public static class MAuthSigningHandlerTests [InlineData("DELETE")] [InlineData("POST")] [InlineData("PUT")] - public static async Task SendAsync_WithNoSigningOptions_WillSignWithOnlyMWSV2(string method) + public static async Task SendAsync_WithNoSignVersions_WillSignWithOnlyMWSV2(string method) { // Arrange var testData = await method.FromResource(); @@ -42,14 +42,14 @@ public static async Task SendAsync_WithNoSigningOptions_WillSignWithOnlyMWSV2(st [InlineData("DELETE")] [InlineData("POST")] [InlineData("PUT")] - public static async Task SendAsync_WithSigningOptionsV1_WillSignWithMWS(string method) + public static async Task SendAsync_WithSignVersionMWS_WillSignWithOnlyMWS(string method) { // Arrange var testData = await method.FromResourceV2(); var actual = new AssertSigningHandler(); var clientOptions = TestExtensions.ClientOptions(testData.SignedTime); - clientOptions.SignVersions = "v1"; var version = MAuthVersion.MWS; + clientOptions.SignVersions = new List { version }; var signingHandler = new MAuthSigningHandler(clientOptions, actual); // Act @@ -71,13 +71,13 @@ public static async Task SendAsync_WithSigningOptionsV1_WillSignWithMWS(string m [InlineData("DELETE")] [InlineData("POST")] [InlineData("PUT")] - public static async Task SendAsync_WithSigningOptionsV1AndV2_WillSignWithBothMWSAndMWSV2(string method) + public static async Task SendAsync_WithSignVersionsMWSAndMWSV2_WillSignWithBothMWSAndMWSV2(string method) { // Arrange var testData = await method.FromResourceV2(); var actual = new AssertSigningHandler(); var clientOptions = TestExtensions.ClientOptions(testData.SignedTime); - clientOptions.SignVersions = "v1,v2"; + clientOptions.SignVersions = new List { MAuthVersion.MWS, MAuthVersion.MWSV2 }; var signingHandler = new MAuthSigningHandler(clientOptions, actual); // Act @@ -93,30 +93,5 @@ public static async Task SendAsync_WithSigningOptionsV1AndV2_WillSignWithBothMWS Assert.Equal(testData.MAuthHeaderV2, actual.MAuthHeaderV2); Assert.Equal(testData.SignedTime, long.Parse(actual.MAuthTimeHeaderV2).FromUnixTimeSeconds()); } - - [Theory] - [InlineData("GET")] - [InlineData("DELETE")] - [InlineData("POST")] - [InlineData("PUT")] - public static async Task SendAsync_WithImproperSigningOptions_WillThrowException(string method) - { - // Arrange - var testData = await method.FromResourceV2(); - var actual = new AssertSigningHandler(); - var clientOptions = TestExtensions.ClientOptions(testData.SignedTime); - clientOptions.SignVersions = "v12"; - var signingHandler = new MAuthSigningHandler(clientOptions, actual); - - // Act - // Assert - using (var client = new HttpClient(signingHandler)) - { - var exception = await Assert.ThrowsAsync(() - => client.SendAsync(testData.ToDefaultHttpRequestMessage())); - Assert.Equal(exception.Message, - $"Signing with {clientOptions.SignVersions} version is not allowed."); - } - } } } From ec3c77e789b17cbfdf61d7465a2f1c334c28e669 Mon Sep 17 00:00:00 2001 From: Prajon Date: Thu, 23 Jul 2020 15:27:27 -0400 Subject: [PATCH 15/21] Updated to use enum parameter of SignVersions instead of list of versions --- README.md | 6 +++--- src/Medidata.MAuth.Core/MAuthSigningHandler.cs | 14 ++++++++------ src/Medidata.MAuth.Core/Models/MAuthVersion.cs | 4 ++-- .../Options/MAuthSigningOptions.cs | 4 ++-- .../MAuthSigningHandlerTests.cs | 5 +++-- 5 files changed, 18 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 6c1fd16..dd209d5 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ public async Task SignAndSendRequest(HttpRequestMessage req PrivateKey = "ClientPrivateKey.pem", // Enumerations of signing protocols, if not provided defaults to `MAuthVersion.MWSV2`for sign-in. - SignVersions = new List { MAuthVersion.MWS, MAuthVersion.MWSV2 } + SignVersions = MAuthVersion.MWS | MAuthVersion.MWSV2 }); using (var client = new HttpClient(signingHandler)) @@ -125,8 +125,8 @@ public async Task SignAndSendRequest(HttpRequestMessage req } ``` The `SignVersions` parameter can be used to specify which protocol version to sign outgoing requests. Like as: -`SignVersions =new List { MAuthVersion.MWS }`: signs with `MWS` protocol only. -`SignVersions =new List { MAuthVersion.MWS, MAuthVersion.MWSV2 }` : signs with both `MWS` and `MWSV2` protocol. +`SignVersions = MAuthVersion.MWS`: signs with `MWS` protocol only. +`SignVersions = MAuthVersion.MWS | MAuthVersion.MWSV2` : signs with both `MWS` and `MWSV2` protocol. If not supplied, it sign by `MWSV2` protocol by default. Signing with `MWSV2` protocol supports query string. diff --git a/src/Medidata.MAuth.Core/MAuthSigningHandler.cs b/src/Medidata.MAuth.Core/MAuthSigningHandler.cs index 08dae45..d1ea387 100644 --- a/src/Medidata.MAuth.Core/MAuthSigningHandler.cs +++ b/src/Medidata.MAuth.Core/MAuthSigningHandler.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -26,7 +25,7 @@ public class MAuthSigningHandler: DelegatingHandler public MAuthSigningHandler(MAuthSigningOptions options) { this.options = options; - this.options.SignVersions = options.SignVersions ?? new List { MAuthVersion.MWSV2 }; + this.options.SignVersions = options.SignVersions ?? MAuthVersion.MWSV2; } /// @@ -40,7 +39,7 @@ public MAuthSigningHandler(MAuthSigningOptions options) public MAuthSigningHandler(MAuthSigningOptions options, HttpMessageHandler innerHandler): base(innerHandler) { this.options = options; - this.options.SignVersions = options.SignVersions ?? new List { MAuthVersion.MWSV2 }; + this.options.SignVersions = options.SignVersions ?? MAuthVersion.MWSV2; } /// @@ -57,10 +56,13 @@ protected override async Task SendAsync( if (InnerHandler == null) InnerHandler = new HttpClientHandler(); - foreach (var version in options.SignVersions) + foreach (MAuthVersion version in Enum.GetValues(typeof(MAuthVersion))) { - var mAuthCore = MAuthCoreFactory.Instantiate(version); - request = await mAuthCore.Sign(request, options).ConfigureAwait(false); + if (options.SignVersions.HasFlag(version)) + { + var mAuthCore = MAuthCoreFactory.Instantiate(version); + request = await mAuthCore.Sign(request, options).ConfigureAwait(false); + } } return await base diff --git a/src/Medidata.MAuth.Core/Models/MAuthVersion.cs b/src/Medidata.MAuth.Core/Models/MAuthVersion.cs index 05ad6be..0fd7568 100644 --- a/src/Medidata.MAuth.Core/Models/MAuthVersion.cs +++ b/src/Medidata.MAuth.Core/Models/MAuthVersion.cs @@ -8,11 +8,11 @@ public enum MAuthVersion /// /// Defines the enumeration value for V1 protocol. /// - MWS, + MWS = 1, /// /// Defines the enumeration value for V2 protocol. /// - MWSV2 + MWSV2 = 2 } } diff --git a/src/Medidata.MAuth.Core/Options/MAuthSigningOptions.cs b/src/Medidata.MAuth.Core/Options/MAuthSigningOptions.cs index 7a9dcf7..1eb11bc 100644 --- a/src/Medidata.MAuth.Core/Options/MAuthSigningOptions.cs +++ b/src/Medidata.MAuth.Core/Options/MAuthSigningOptions.cs @@ -24,8 +24,8 @@ public class MAuthSigningOptions internal DateTimeOffset? SignedTime { get; set; } /// - /// Enumeration of MAuth protocol versions to sign requests, if not provided defaults to "v2". + /// Enumeration values of MAuth protocol versions to sign requests, if not provided defaults to `MWSV2`. /// - public IEnumerable SignVersions { get; set; } + public Enum SignVersions { get; set; } } } diff --git a/tests/Medidata.MAuth.Tests/MAuthSigningHandlerTests.cs b/tests/Medidata.MAuth.Tests/MAuthSigningHandlerTests.cs index 33666fe..e6ba0e4 100644 --- a/tests/Medidata.MAuth.Tests/MAuthSigningHandlerTests.cs +++ b/tests/Medidata.MAuth.Tests/MAuthSigningHandlerTests.cs @@ -49,7 +49,7 @@ public static async Task SendAsync_WithSignVersionMWS_WillSignWithOnlyMWS(string var actual = new AssertSigningHandler(); var clientOptions = TestExtensions.ClientOptions(testData.SignedTime); var version = MAuthVersion.MWS; - clientOptions.SignVersions = new List { version }; + clientOptions.SignVersions = version; var signingHandler = new MAuthSigningHandler(clientOptions, actual); // Act @@ -77,7 +77,8 @@ public static async Task SendAsync_WithSignVersionsMWSAndMWSV2_WillSignWithBothM var testData = await method.FromResourceV2(); var actual = new AssertSigningHandler(); var clientOptions = TestExtensions.ClientOptions(testData.SignedTime); - clientOptions.SignVersions = new List { MAuthVersion.MWS, MAuthVersion.MWSV2 }; + clientOptions.SignVersions = MAuthVersion.MWS | MAuthVersion.MWSV2; + var signingHandler = new MAuthSigningHandler(clientOptions, actual); // Act From a27dee0a9e360886558554a30071a3bc51ecbdc2 Mon Sep 17 00:00:00 2001 From: Prajon Date: Mon, 27 Jul 2020 09:05:08 -0400 Subject: [PATCH 16/21] Updated `MAuthVersion` enumeration to use `Flags` attribute --- src/Medidata.MAuth.Core/MAuthSigningHandler.cs | 4 ++-- src/Medidata.MAuth.Core/Models/MAuthVersion.cs | 5 ++++- src/Medidata.MAuth.Core/Options/MAuthSigningOptions.cs | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Medidata.MAuth.Core/MAuthSigningHandler.cs b/src/Medidata.MAuth.Core/MAuthSigningHandler.cs index d1ea387..80e3735 100644 --- a/src/Medidata.MAuth.Core/MAuthSigningHandler.cs +++ b/src/Medidata.MAuth.Core/MAuthSigningHandler.cs @@ -25,7 +25,7 @@ public class MAuthSigningHandler: DelegatingHandler public MAuthSigningHandler(MAuthSigningOptions options) { this.options = options; - this.options.SignVersions = options.SignVersions ?? MAuthVersion.MWSV2; + this.options.SignVersions = options.SignVersions; } /// @@ -39,7 +39,7 @@ public MAuthSigningHandler(MAuthSigningOptions options) public MAuthSigningHandler(MAuthSigningOptions options, HttpMessageHandler innerHandler): base(innerHandler) { this.options = options; - this.options.SignVersions = options.SignVersions ?? MAuthVersion.MWSV2; + this.options.SignVersions = options.SignVersions; } /// diff --git a/src/Medidata.MAuth.Core/Models/MAuthVersion.cs b/src/Medidata.MAuth.Core/Models/MAuthVersion.cs index 0fd7568..45c3197 100644 --- a/src/Medidata.MAuth.Core/Models/MAuthVersion.cs +++ b/src/Medidata.MAuth.Core/Models/MAuthVersion.cs @@ -1,8 +1,11 @@ -namespace Medidata.MAuth.Core.Models +using System; + +namespace Medidata.MAuth.Core.Models { /// /// Contains the Enumeration values for different versions supported by the library. /// + [Flags] public enum MAuthVersion { /// diff --git a/src/Medidata.MAuth.Core/Options/MAuthSigningOptions.cs b/src/Medidata.MAuth.Core/Options/MAuthSigningOptions.cs index 1eb11bc..db5b174 100644 --- a/src/Medidata.MAuth.Core/Options/MAuthSigningOptions.cs +++ b/src/Medidata.MAuth.Core/Options/MAuthSigningOptions.cs @@ -26,6 +26,6 @@ public class MAuthSigningOptions /// /// Enumeration values of MAuth protocol versions to sign requests, if not provided defaults to `MWSV2`. /// - public Enum SignVersions { get; set; } + public MAuthVersion SignVersions { get; set; } = MAuthVersion.MWSV2; } } From 0b05592c3fa3f0e976324e5793da588fcde16a92 Mon Sep 17 00:00:00 2001 From: Prajon Date: Mon, 27 Jul 2020 15:52:07 -0400 Subject: [PATCH 17/21] Updated the default signing to "V1" protocol. --- CHANGELOG.md | 2 +- README.md | 8 ++++---- .../Options/MAuthSigningOptions.cs | 4 ++-- .../Medidata.MAuth.Tests/MAuthSigningHandlerTests.cs | 12 ++++++------ 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0270c30..c9b9823 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ## v5.0.0 - **[Core]** Added normalization of Uri AbsolutePath. - **[Core]** Added unescape step in query_string encoding to remove `double encoding`. - - **[Core]** Replace `DisableV1`option with `SignVersions` option and change the default signing to `MAuthVersion.MWSV2` only. + - **[Core]** Replace `DisableV1`option with `SignVersions` option and change the default signing to `MAuthVersion.MWS` only. ## v4.0.2 - **[AspNetCore]** Update aspnetcore version to aspnetcore2.1 LTS. diff --git a/README.md b/README.md index dd209d5..daf4c99 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ public async Task SignAndSendRequest(HttpRequestMessage req // The following can be either a path to the key file or the contents of the file itself PrivateKey = "ClientPrivateKey.pem", - // Enumerations of signing protocols, if not provided defaults to `MAuthVersion.MWSV2`for sign-in. + // Enumerations of signing protocols, if not provided defaults to `MAuthVersion.MWS`for sign-in. SignVersions = MAuthVersion.MWS | MAuthVersion.MWSV2 }); @@ -127,7 +127,7 @@ public async Task SignAndSendRequest(HttpRequestMessage req The `SignVersions` parameter can be used to specify which protocol version to sign outgoing requests. Like as: `SignVersions = MAuthVersion.MWS`: signs with `MWS` protocol only. `SignVersions = MAuthVersion.MWS | MAuthVersion.MWSV2` : signs with both `MWS` and `MWSV2` protocol. -If not supplied, it sign by `MWSV2` protocol by default. +If not supplied, it sign by `MWS` protocol by default. Signing with `MWSV2` protocol supports query string. @@ -140,7 +140,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. | -| **SignVersions** | (optional) Enumerations of MAuth protocol versions to sign requests. If not supplied, defaults to `MWSV2`. +| **SignVersions** | (optional) Enumerations of MAuth protocol versions to sign requests. If not supplied, defaults to `MWS`. ### Authenticating Incoming Requests with the OWIN and ASP.NET Core Middlewares @@ -334,7 +334,7 @@ the portable fork of the [BouncyCastle](https://github.com/onovotny/BouncyCastle ##### What are the major changes in the 5.0.0 version? In this version we have removed the property `DisableV1` from `MAuthSigningOptions`. Instead, we have added new option as `SignVersions` in `MAuthSigningOptions` which takes enumeration values of MAuth protcol versions `MWS` and/ or `MWSV2` protocol. -If this option is not provided, then it will sign in by `MWSV2` protocol as default. +If this option is not provided, then it will sign in by `MWS` protocol as default. ##### What are the major changes in the 4.0.0 version? diff --git a/src/Medidata.MAuth.Core/Options/MAuthSigningOptions.cs b/src/Medidata.MAuth.Core/Options/MAuthSigningOptions.cs index db5b174..cc3c894 100644 --- a/src/Medidata.MAuth.Core/Options/MAuthSigningOptions.cs +++ b/src/Medidata.MAuth.Core/Options/MAuthSigningOptions.cs @@ -24,8 +24,8 @@ public class MAuthSigningOptions internal DateTimeOffset? SignedTime { get; set; } /// - /// Enumeration values of MAuth protocol versions to sign requests, if not provided defaults to `MWSV2`. + /// Enumeration values of MAuth protocol versions to sign requests, if not provided defaults to `MWS`. /// - public MAuthVersion SignVersions { get; set; } = MAuthVersion.MWSV2; + public MAuthVersion SignVersions { get; set; } = MAuthVersion.MWS; } } diff --git a/tests/Medidata.MAuth.Tests/MAuthSigningHandlerTests.cs b/tests/Medidata.MAuth.Tests/MAuthSigningHandlerTests.cs index e6ba0e4..4012633 100644 --- a/tests/Medidata.MAuth.Tests/MAuthSigningHandlerTests.cs +++ b/tests/Medidata.MAuth.Tests/MAuthSigningHandlerTests.cs @@ -15,12 +15,12 @@ public static class MAuthSigningHandlerTests [InlineData("DELETE")] [InlineData("POST")] [InlineData("PUT")] - public static async Task SendAsync_WithNoSignVersions_WillSignWithOnlyMWSV2(string method) + public static async Task SendAsync_WithNoSignVersions_WillSignWithOnlyMWS(string method) { // Arrange var testData = await method.FromResource(); var actual = new AssertSigningHandler(); - var version = MAuthVersion.MWSV2; + var version = MAuthVersion.MWS; var signingHandler = new MAuthSigningHandler(TestExtensions.ClientOptions(testData.SignedTime), actual); // Act @@ -30,11 +30,11 @@ public static async Task SendAsync_WithNoSignVersions_WillSignWithOnlyMWSV2(stri } // Assert - Assert.Null(actual.MAuthHeader); - Assert.Null(actual.MAuthTimeHeader); + Assert.Null(actual.MAuthHeaderV2); + Assert.Null(actual.MAuthTimeHeaderV2); - Assert.Equal(testData.MAuthHeaderV2, actual.MAuthHeaderV2); - Assert.Equal(testData.SignedTime, long.Parse(actual.MAuthTimeHeaderV2).FromUnixTimeSeconds()); + Assert.Equal(testData.MAuthHeader, actual.MAuthHeader); + Assert.Equal(testData.SignedTime, long.Parse(actual.MAuthTimeHeader).FromUnixTimeSeconds()); } [Theory] From 38093290d8aecd5d259f36f11440e33acd8ec961 Mon Sep 17 00:00:00 2001 From: Prajon Date: Tue, 28 Jul 2020 10:38:49 -0400 Subject: [PATCH 18/21] Removed unnecessary line --- src/Medidata.MAuth.Core/MAuthSigningHandler.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Medidata.MAuth.Core/MAuthSigningHandler.cs b/src/Medidata.MAuth.Core/MAuthSigningHandler.cs index 80e3735..8e5c294 100644 --- a/src/Medidata.MAuth.Core/MAuthSigningHandler.cs +++ b/src/Medidata.MAuth.Core/MAuthSigningHandler.cs @@ -25,7 +25,6 @@ public class MAuthSigningHandler: DelegatingHandler public MAuthSigningHandler(MAuthSigningOptions options) { this.options = options; - this.options.SignVersions = options.SignVersions; } /// @@ -39,7 +38,6 @@ public MAuthSigningHandler(MAuthSigningOptions options) public MAuthSigningHandler(MAuthSigningOptions options, HttpMessageHandler innerHandler): base(innerHandler) { this.options = options; - this.options.SignVersions = options.SignVersions; } /// From 941cff60d6be1d4e0dd10544e166a41b7bfc6c41 Mon Sep 17 00:00:00 2001 From: Prajon Date: Tue, 28 Jul 2020 11:06:00 -0400 Subject: [PATCH 19/21] Updated private variable prefixed with underscore --- src/Medidata.MAuth.Core/MAuthSigningHandler.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Medidata.MAuth.Core/MAuthSigningHandler.cs b/src/Medidata.MAuth.Core/MAuthSigningHandler.cs index 8e5c294..0885a72 100644 --- a/src/Medidata.MAuth.Core/MAuthSigningHandler.cs +++ b/src/Medidata.MAuth.Core/MAuthSigningHandler.cs @@ -12,10 +12,10 @@ namespace Medidata.MAuth.Core /// public class MAuthSigningHandler: DelegatingHandler { - private readonly MAuthSigningOptions options; + private readonly MAuthSigningOptions _options; /// Gets the Uuid of the client application. - public Guid ClientAppUuid => options.ApplicationUuid; + public Guid ClientAppUuid => _options.ApplicationUuid; /// /// Initializes a new instance of the class with the provided @@ -24,7 +24,7 @@ public class MAuthSigningHandler: DelegatingHandler /// The options for this message handler. public MAuthSigningHandler(MAuthSigningOptions options) { - this.options = options; + _options = options; } /// @@ -37,7 +37,7 @@ public MAuthSigningHandler(MAuthSigningOptions options) /// public MAuthSigningHandler(MAuthSigningOptions options, HttpMessageHandler innerHandler): base(innerHandler) { - this.options = options; + _options = options; } /// @@ -56,10 +56,10 @@ protected override async Task SendAsync( foreach (MAuthVersion version in Enum.GetValues(typeof(MAuthVersion))) { - if (options.SignVersions.HasFlag(version)) + if (_options.SignVersions.HasFlag(version)) { var mAuthCore = MAuthCoreFactory.Instantiate(version); - request = await mAuthCore.Sign(request, options).ConfigureAwait(false); + request = await mAuthCore.Sign(request, _options).ConfigureAwait(false); } } From 7ebb4941e8495cd4c68cbfac05039bb879929454 Mon Sep 17 00:00:00 2001 From: Prajon Date: Tue, 28 Jul 2020 11:24:13 -0400 Subject: [PATCH 20/21] Prefix private variables with "_" and remove "this" keyword. --- .../MAuthMiddleware.cs | 18 ++++---- src/Medidata.MAuth.Core/MAuthAuthenticator.cs | 42 +++++++++---------- src/Medidata.MAuth.Owin/MAuthMiddleware.cs | 13 +++--- .../MAuthAuthenticatingHandler.cs | 20 ++++----- .../Infrastructure/NonSeekableStream.cs | 16 +++---- .../RequestBodyAsNonSeekableMiddleware.cs | 6 +-- 6 files changed, 58 insertions(+), 57 deletions(-) diff --git a/src/Medidata.MAuth.AspNetCore/MAuthMiddleware.cs b/src/Medidata.MAuth.AspNetCore/MAuthMiddleware.cs index 9f885b1..cade823 100644 --- a/src/Medidata.MAuth.AspNetCore/MAuthMiddleware.cs +++ b/src/Medidata.MAuth.AspNetCore/MAuthMiddleware.cs @@ -12,9 +12,9 @@ namespace Medidata.MAuth.AspNetCore /// internal class MAuthMiddleware { - private readonly MAuthMiddlewareOptions options; - private readonly MAuthAuthenticator authenticator; - private readonly RequestDelegate next; + private readonly MAuthMiddlewareOptions _options; + private readonly MAuthAuthenticator _authenticator; + private readonly RequestDelegate _next; /// /// Creates a new @@ -24,11 +24,11 @@ internal class MAuthMiddleware /// The representing the factory that used to create logger instances. public MAuthMiddleware(RequestDelegate next, MAuthMiddlewareOptions options, ILoggerFactory loggerFactory) { - this.next = next; - this.options = options; + _next = next; + _options = options; loggerFactory = loggerFactory ?? NullLoggerFactory.Instance; ILogger logger = loggerFactory.CreateLogger(); - this.authenticator = new MAuthAuthenticator(options, logger); + _authenticator = new MAuthAuthenticator(options, logger); } /// @@ -40,8 +40,8 @@ public async Task Invoke(HttpContext context) { context.Request.EnableBuffering(); - if (!options.Bypass(context.Request) && - !await context.TryAuthenticate(authenticator, options.HideExceptionsAndReturnUnauthorized).ConfigureAwait(false)) + if (!_options.Bypass(context.Request) && + !await context.TryAuthenticate(_authenticator, _options.HideExceptionsAndReturnUnauthorized).ConfigureAwait(false)) { context.Response.StatusCode = (int)HttpStatusCode.Unauthorized; return; @@ -49,7 +49,7 @@ public async Task Invoke(HttpContext context) context.Request.Body.Rewind(); - await next.Invoke(context).ConfigureAwait(false); + await _next.Invoke(context).ConfigureAwait(false); } } } diff --git a/src/Medidata.MAuth.Core/MAuthAuthenticator.cs b/src/Medidata.MAuth.Core/MAuthAuthenticator.cs index 564d470..440ee95 100644 --- a/src/Medidata.MAuth.Core/MAuthAuthenticator.cs +++ b/src/Medidata.MAuth.Core/MAuthAuthenticator.cs @@ -12,11 +12,11 @@ namespace Medidata.MAuth.Core { internal class MAuthAuthenticator { - private readonly MAuthOptionsBase options; - private readonly IMemoryCache cache = new MemoryCache(new MemoryCacheOptions()); - private readonly ILogger logger; + private readonly MAuthOptionsBase _options; + private readonly IMemoryCache _cache = new MemoryCache(new MemoryCacheOptions()); + private readonly ILogger _logger; - public Guid ApplicationUuid => options.ApplicationUuid; + public Guid ApplicationUuid => _options.ApplicationUuid; public MAuthAuthenticator(MAuthOptionsBase options, ILogger logger) { @@ -29,8 +29,8 @@ public MAuthAuthenticator(MAuthOptionsBase options, ILogger logger) if (string.IsNullOrWhiteSpace(options.PrivateKey)) throw new ArgumentNullException(nameof(options.PrivateKey)); - this.options = options; - this.logger = logger; + _options = options; + _logger = logger; } /// @@ -42,49 +42,49 @@ public async Task AuthenticateRequest(HttpRequestMessage request) { try { - logger.LogInformation("Initiating Authentication of the request."); + _logger.LogInformation("Initiating Authentication of the request."); var authHeader = request.GetAuthHeaderValue(); var version = authHeader.GetVersionFromAuthenticationHeader(); var parsedHeader = authHeader.ParseAuthenticationHeader(); - if (options.DisableV1 && version == MAuthVersion.MWS) + if (_options.DisableV1 && version == MAuthVersion.MWS) throw new InvalidVersionException($"Authentication with {version} version is disabled."); var authenticated = await Authenticate(request, version, parsedHeader.Uuid).ConfigureAwait(false); - if (!authenticated && version == MAuthVersion.MWSV2 && !options.DisableV1) + if (!authenticated && version == MAuthVersion.MWSV2 && !_options.DisableV1) { // fall back to V1 authentication authenticated = await Authenticate(request, MAuthVersion.MWS, parsedHeader.Uuid).ConfigureAwait(false); - logger.LogWarning("Completed successful authentication attempt after fallback to V1"); + _logger.LogWarning("Completed successful authentication attempt after fallback to V1"); } return authenticated; } catch (ArgumentException ex) { - logger.LogError(ex, "Unable to authenticate due to invalid MAuth authentication headers."); + _logger.LogError(ex, "Unable to authenticate due to invalid MAuth authentication headers."); throw new AuthenticationException("The request has invalid MAuth authentication headers.", ex); } catch (RetriedRequestException ex) { - logger.LogError(ex, "Unable to query the application information from MAuth server."); + _logger.LogError(ex, "Unable to query the application information from MAuth server."); throw new AuthenticationException( "Could not query the application information for the application from the MAuth server.", ex); } catch (InvalidCipherTextException ex) { - logger.LogWarning(ex, "Unable to authenticate due to invalid payload information."); + _logger.LogWarning(ex, "Unable to authenticate due to invalid payload information."); throw new AuthenticationException( "The request verification failed due to an invalid payload information.", ex); } catch (InvalidVersionException ex) { - logger.LogError(ex, "Unable to authenticate due to invalid version."); + _logger.LogError(ex, "Unable to authenticate due to invalid version."); throw new InvalidVersionException(ex.Message, ex); } catch (Exception ex) { - logger.LogError(ex, "Unable to authenticate due to unexpected error."); + _logger.LogError(ex, "Unable to authenticate due to unexpected error."); throw new AuthenticationException( "An unexpected error occured during authentication. Please see the inner exception for details.", ex @@ -95,8 +95,8 @@ public async Task AuthenticateRequest(HttpRequestMessage request) private async Task Authenticate(HttpRequestMessage request, MAuthVersion version, Guid signedAppUuid) { var logMessage = "Mauth-client attempting to authenticate request from app with mauth app uuid " + - $"{signedAppUuid} to app with mauth app uuid {options.ApplicationUuid} using version {version}"; - logger.LogInformation(logMessage); + $"{signedAppUuid} to app with mauth app uuid {_options.ApplicationUuid} using version {version}"; + _logger.LogInformation(logMessage); var mAuthCore = MAuthCoreFactory.Instantiate(version); var authInfo = GetAuthenticationInfo(request, mAuthCore); @@ -107,13 +107,13 @@ private async Task Authenticate(HttpRequestMessage request, MAuthVersion v } private Task GetApplicationInfo(Guid applicationUuid) => - cache.GetOrCreateAsync(applicationUuid, async entry => + _cache.GetOrCreateAsync(applicationUuid, async entry => { - var retrier = new MAuthRequestRetrier(options); + var retrier = new MAuthRequestRetrier(_options); var response = await retrier.GetSuccessfulResponse( applicationUuid, CreateRequest, - requestAttempts: (int)options.MAuthServiceRetryPolicy + 1 + requestAttempts: (int)_options.MAuthServiceRetryPolicy + 1 ).ConfigureAwait(false); var result = await response.Content.FromResponse().ConfigureAwait(false); @@ -160,7 +160,7 @@ internal static PayloadAuthenticationInfo GetAuthenticationInfo(HttpRequestMessa } private HttpRequestMessage CreateRequest(Guid applicationUuid) => - new HttpRequestMessage(HttpMethod.Get, new Uri(options.MAuthServiceUrl, + new HttpRequestMessage(HttpMethod.Get, new Uri(_options.MAuthServiceUrl, $"{Constants.MAuthTokenRequestPath}{applicationUuid.ToHyphenString()}.json")); } } diff --git a/src/Medidata.MAuth.Owin/MAuthMiddleware.cs b/src/Medidata.MAuth.Owin/MAuthMiddleware.cs index b15584c..2ff9317 100644 --- a/src/Medidata.MAuth.Owin/MAuthMiddleware.cs +++ b/src/Medidata.MAuth.Owin/MAuthMiddleware.cs @@ -8,21 +8,22 @@ namespace Medidata.MAuth.Owin { internal class MAuthMiddleware: OwinMiddleware { - private readonly MAuthMiddlewareOptions options; - private readonly MAuthAuthenticator authenticator; + private readonly MAuthMiddlewareOptions _options; + private readonly MAuthAuthenticator _authenticator; public MAuthMiddleware(OwinMiddleware next, MAuthMiddlewareOptions options, ILogger owinLogger) : base(next) { - this.options = options; + _options = options; Microsoft.Extensions.Logging.ILogger logger = new OwinLoggerWrapper(owinLogger); - authenticator = new MAuthAuthenticator(options, logger); + _authenticator = new MAuthAuthenticator(options, logger); } public override async Task Invoke(IOwinContext context) { await context.EnsureRequestBodyStreamSeekable().ConfigureAwait(false); - if (!options.Bypass(context.Request) && - !await context.TryAuthenticate(authenticator, options.HideExceptionsAndReturnUnauthorized).ConfigureAwait(false)) + if (!_options.Bypass(context.Request) && + !await context.TryAuthenticate(_authenticator, _options.HideExceptionsAndReturnUnauthorized) + .ConfigureAwait(false)) { context.Response.StatusCode = (int)HttpStatusCode.Unauthorized; return; diff --git a/src/Medidata.MAuth.WebApi/MAuthAuthenticatingHandler.cs b/src/Medidata.MAuth.WebApi/MAuthAuthenticatingHandler.cs index a556b6a..6f07516 100644 --- a/src/Medidata.MAuth.WebApi/MAuthAuthenticatingHandler.cs +++ b/src/Medidata.MAuth.WebApi/MAuthAuthenticatingHandler.cs @@ -15,11 +15,11 @@ namespace Medidata.MAuth.WebApi /// public class MAuthAuthenticatingHandler : DelegatingHandler { - private readonly MAuthWebApiOptions options; - private readonly MAuthAuthenticator authenticator; + private readonly MAuthWebApiOptions _options; + private readonly MAuthAuthenticator _authenticator; /// Gets the Uuid of the client application. - public Guid ClientAppUuid => authenticator.ApplicationUuid; + public Guid ClientAppUuid => _authenticator.ApplicationUuid; /// /// Initializes a new instance of the class with the provided @@ -28,8 +28,8 @@ public class MAuthAuthenticatingHandler : DelegatingHandler /// The options for this message handler. public MAuthAuthenticatingHandler(MAuthWebApiOptions options) { - this.options = options; - this.authenticator = this.SetupMAuthAuthenticator(options); + _options = options; + _authenticator = SetupMAuthAuthenticator(options); } /// @@ -42,8 +42,8 @@ public MAuthAuthenticatingHandler(MAuthWebApiOptions options) /// public MAuthAuthenticatingHandler(MAuthWebApiOptions options, HttpMessageHandler innerHandler) : base(innerHandler) { - this.options = options; - this.authenticator = this.SetupMAuthAuthenticator(options); + _options = options; + _authenticator = SetupMAuthAuthenticator(options); } /// @@ -60,7 +60,7 @@ protected override async Task SendAsync( if (InnerHandler == null) InnerHandler = new HttpClientHandler(); - if (!await request.TryAuthenticate(authenticator, options.HideExceptionsAndReturnUnauthorized).ConfigureAwait(false)) + if (!await request.TryAuthenticate(_authenticator, _options.HideExceptionsAndReturnUnauthorized).ConfigureAwait(false)) return new HttpResponseMessage(HttpStatusCode.Unauthorized) { RequestMessage = request }; return await base @@ -70,9 +70,9 @@ protected override async Task SendAsync( private MAuthAuthenticator SetupMAuthAuthenticator(MAuthWebApiOptions opt) { - var loggerFactory = options.LoggerFactory ?? NullLoggerFactory.Instance; + var loggerFactory = _options.LoggerFactory ?? NullLoggerFactory.Instance; var logger = loggerFactory.CreateLogger(typeof(MAuthAuthenticatingHandler)); - return new MAuthAuthenticator(options, logger); + return new MAuthAuthenticator(_options, logger); } } } diff --git a/tests/Medidata.MAuth.Tests/Infrastructure/NonSeekableStream.cs b/tests/Medidata.MAuth.Tests/Infrastructure/NonSeekableStream.cs index 8514c54..d67dad9 100644 --- a/tests/Medidata.MAuth.Tests/Infrastructure/NonSeekableStream.cs +++ b/tests/Medidata.MAuth.Tests/Infrastructure/NonSeekableStream.cs @@ -5,16 +5,16 @@ namespace Medidata.MAuth.Tests.Infrastructure { internal class NonSeekableStream: Stream { - private readonly Stream baseStream; + private readonly Stream _baseStream; public override bool CanSeek => false; - public override bool CanRead => baseStream.CanRead; + public override bool CanRead => _baseStream.CanRead; - public override bool CanWrite => baseStream.CanWrite; + public override bool CanWrite => _baseStream.CanWrite; - public override long Length => baseStream.Length; + public override long Length => _baseStream.Length; public override long Position { @@ -22,16 +22,16 @@ public override long Position set => throw new NotSupportedException(); } - public NonSeekableStream(Stream baseStream) => this.baseStream = baseStream; + public NonSeekableStream(Stream baseStream) => _baseStream = baseStream; public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); - public override void Flush() => baseStream.Flush(); + public override void Flush() => _baseStream.Flush(); public override void SetLength(long value) => throw new NotSupportedException(); - public override int Read(byte[] buffer, int offset, int count) => baseStream.Read(buffer, offset, count); + public override int Read(byte[] buffer, int offset, int count) => _baseStream.Read(buffer, offset, count); - public override void Write(byte[] buffer, int offset, int count) => baseStream.Write(buffer, offset, count); + public override void Write(byte[] buffer, int offset, int count) => _baseStream.Write(buffer, offset, count); } } diff --git a/tests/Medidata.MAuth.Tests/Infrastructure/RequestBodyAsNonSeekableMiddleware.cs b/tests/Medidata.MAuth.Tests/Infrastructure/RequestBodyAsNonSeekableMiddleware.cs index 25b70bb..f191e4a 100644 --- a/tests/Medidata.MAuth.Tests/Infrastructure/RequestBodyAsNonSeekableMiddleware.cs +++ b/tests/Medidata.MAuth.Tests/Infrastructure/RequestBodyAsNonSeekableMiddleware.cs @@ -6,9 +6,9 @@ namespace Medidata.MAuth.Tests.Infrastructure { internal class RequestBodyAsNonSeekableMiddleware { - private readonly RequestDelegate next; + private readonly RequestDelegate _next; - public RequestBodyAsNonSeekableMiddleware(RequestDelegate next) => this.next = next; + public RequestBodyAsNonSeekableMiddleware(RequestDelegate next) => _next = next; public async Task Invoke(HttpContext context) { @@ -18,7 +18,7 @@ public async Task Invoke(HttpContext context) context.Request.Body = body; } - await next.Invoke(context); + await _next.Invoke(context); } } } From 50dfbd964d32a5ff4752bb8f9443aedb2a2c4870 Mon Sep 17 00:00:00 2001 From: Prajon Shakya Date: Thu, 6 Aug 2020 10:03:47 -0400 Subject: [PATCH 21/21] [MCC-575820] Incorporate protocol test suite and implement (#64) --- CHANGELOG.md | 2 + CONTRIBUTING.md | 25 ++++ build/build.ps1 | 2 +- .../MAuthCoreExtensions.cs | 24 ++- .../Infrastructure/AssertSigningHandler.cs | 2 +- .../Infrastructure/MAuthServerHandler.cs | 43 +++++- .../Infrastructure/TestExtensions.cs | 16 +- .../MAuthAspNetCoreTests.cs | 13 +- .../MAuthAuthenticatorTests.cs | 33 +++-- tests/Medidata.MAuth.Tests/MAuthOwinTests.cs | 12 +- .../MAuthProtocolSuiteTests.cs | 124 ++++++++++++++++ .../Medidata.MAuth.Tests/MAuthWebApiTests.cs | 13 +- .../ProtocolTestSuite/AuthenticationHeader.cs | 14 ++ .../ProtocolTestSuiteHelper.cs | 139 ++++++++++++++++++ .../ProtocolTestSuite/SigningConfig.cs | 19 +++ .../ProtocolTestSuite/UnSignedRequest.cs | 19 +++ .../UtilityExtensionsTest.cs | 3 +- 17 files changed, 443 insertions(+), 60 deletions(-) create mode 100644 CONTRIBUTING.md create mode 100644 tests/Medidata.MAuth.Tests/MAuthProtocolSuiteTests.cs create mode 100644 tests/Medidata.MAuth.Tests/ProtocolTestSuite/AuthenticationHeader.cs create mode 100644 tests/Medidata.MAuth.Tests/ProtocolTestSuite/ProtocolTestSuiteHelper.cs create mode 100644 tests/Medidata.MAuth.Tests/ProtocolTestSuite/SigningConfig.cs create mode 100644 tests/Medidata.MAuth.Tests/ProtocolTestSuite/UnSignedRequest.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index c9b9823..240267f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ - **[Core]** Added normalization of Uri AbsolutePath. - **[Core]** Added unescape step in query_string encoding to remove `double encoding`. - **[Core]** Replace `DisableV1`option with `SignVersions` option and change the default signing to `MAuthVersion.MWS` only. + - **[Core]** Added parsing code to test with mauth-protocol-test-suite. + - **[Core]** Fixed bug in sorting of query parameters. ## v4.0.2 - **[AspNetCore]** Update aspnetcore version to aspnetcore2.1 LTS. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..1218bdd --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,25 @@ +# Contributing + +## General Information +* Clone this repo in your workspace. Checkout latest `develop` branch. +* Make new changes or updates into `feature/bugfix` branch. +* Make sure to add unit tests for it so that there is no breaking changes. +* Commit and push your branch to compare and create PR against latest `develop` branch. + +## Running Tests +To run tests, go the folder `mauth-client-dotnet\tests\Medidata.MAuth.Tests` +Next, run the tests as: + +``` +dotnet test --filter "Category!=ProtocolTestSuite" +``` + +## Running mauth-protocol-test-suite +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 +`TEST_SUITE_PATH` with the path to the test suite relative to this repo). +Then navigate to :`mauth-client-dotnet\tests\Medidata.MAuth.Tests` +And, run the tests as: + +``` +dotnet test --filter "Category=ProtocolTestSuite" +``` diff --git a/build/build.ps1 b/build/build.ps1 index 91ddcb0..af8c986 100644 --- a/build/build.ps1 +++ b/build/build.ps1 @@ -67,7 +67,7 @@ Write-Host "Running unit tests..." -ForegroundColor Cyan Push-Location -Path .\tests\Medidata.MAuth.Tests -dotnet test +dotnet test --filter "Category!=ProtocolTestSuite" Pop-Location diff --git a/src/Medidata.MAuth.Core/MAuthCoreExtensions.cs b/src/Medidata.MAuth.Core/MAuthCoreExtensions.cs index 209e0bb..54f68d1 100644 --- a/src/Medidata.MAuth.Core/MAuthCoreExtensions.cs +++ b/src/Medidata.MAuth.Core/MAuthCoreExtensions.cs @@ -188,33 +188,27 @@ public static string BuildEncodedQueryParams(this string queryString) return string.Empty; var queryArray = queryString.Split('&'); + var unescapedKeysAndValues = new KeyValuePair[queryArray.Length]; // unescaping for (int i = 0; i < queryArray.Length; i++) { var keyValue = queryArray[i].Split('='); - var unEscapedKey = Uri.UnescapeDataString(keyValue[0]); - var unEscapedValue = Uri.UnescapeDataString(keyValue[1]); - queryArray[i] = $"{unEscapedKey}={unEscapedValue}"; + unescapedKeysAndValues[i] = new KeyValuePair( + Uri.UnescapeDataString(keyValue[0]), Uri.UnescapeDataString(keyValue[1])); } - // sorting - Array.Sort(queryArray, StringComparer.Ordinal); - - // escaping - for (int i = 0; i < queryArray.Length; i++) - { - var keyValue = queryArray[i].Split('='); - var escapedKey = Uri.EscapeDataString(keyValue[0]); - var escapedValue = Uri.EscapeDataString(keyValue[1]); - queryArray[i] = $"{escapedKey}={escapedValue}"; - } + // sorting and escaping + var escapedKeyValues = unescapedKeysAndValues + .OrderBy(kv => kv.Key, StringComparer.Ordinal) + .ThenBy(kv => kv.Value, StringComparer.Ordinal) + .Select(kv => $"{Uri.EscapeDataString(kv.Key)}={Uri.EscapeDataString(kv.Value)}"); // Above encoding converts space as `%20` and `+` as `%2B` // But space and `+` both needs to be converted as `%20` as per // reference https://github.com/mdsol/mauth-client-ruby/blob/v6.0.0/lib/mauth/request_and_response.rb#L113 // so this convert `%2B` into `%20` to match encodedqueryparams to that of other languages. - return string.Join("&", queryArray).Replace("%2B", "%20"); + return string.Join("&", escapedKeyValues).Replace("%2B", "%20"); } /// diff --git a/tests/Medidata.MAuth.Tests/Infrastructure/AssertSigningHandler.cs b/tests/Medidata.MAuth.Tests/Infrastructure/AssertSigningHandler.cs index e390003..1290cf8 100644 --- a/tests/Medidata.MAuth.Tests/Infrastructure/AssertSigningHandler.cs +++ b/tests/Medidata.MAuth.Tests/Infrastructure/AssertSigningHandler.cs @@ -29,7 +29,7 @@ protected override Task SendAsync( MAuthTimeHeaderV2 = request.Headers.GetFirstValueOrDefault(Constants.MAuthTimeHeaderKeyV2); MAuthTimeHeader = request.Headers.GetFirstValueOrDefault(Constants.MAuthTimeHeaderKey); - return Task.Run(() => new HttpResponseMessage(HttpStatusCode.OK)); + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)); } } } diff --git a/tests/Medidata.MAuth.Tests/Infrastructure/MAuthServerHandler.cs b/tests/Medidata.MAuth.Tests/Infrastructure/MAuthServerHandler.cs index 3499c89..b5ac3ed 100644 --- a/tests/Medidata.MAuth.Tests/Infrastructure/MAuthServerHandler.cs +++ b/tests/Medidata.MAuth.Tests/Infrastructure/MAuthServerHandler.cs @@ -4,26 +4,45 @@ using System.Threading; using System.Threading.Tasks; using Medidata.MAuth.Core; -using Microsoft.Extensions.Logging.Abstractions; +using Medidata.MAuth.Tests.ProtocolTestSuite; using Newtonsoft.Json; namespace Medidata.MAuth.Tests.Infrastructure { internal class MAuthServerHandler : HttpMessageHandler { - private static readonly Guid clientUuid = new Guid("192cce84-8466-490e-b03e-074f82da3ee2"); - private int currentNumberOfAttempts = 0; + private MAuthServerHandler() { } + + private static readonly Guid _clientUuid = new Guid("192cce84-8466-490e-b03e-074f82da3ee2"); + private int _currentNumberOfAttempts = 0; public int SucceedAfterThisManyAttempts { get; set; } = 1; + private static string _signingPublicKey; + private static Guid _signingAppUuid; + + private async Task InitializeAsync() + { + var protocolSuite = new ProtocolTestSuiteHelper(); + _signingPublicKey = await protocolSuite.GetPublicKey(); + _signingAppUuid = await protocolSuite.ReadSignInAppUuid(); + return this; + } + + public static Task CreateAsync() + { + var ret = new MAuthServerHandler(); + return ret.InitializeAsync(); + } + protected override async Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { - currentNumberOfAttempts += 1; + _currentNumberOfAttempts += 1; var version = request.GetAuthHeaderValue().GetVersionFromAuthenticationHeader(); var mAuthCore = MAuthCoreFactory.Instantiate(version); - if (currentNumberOfAttempts < SucceedAfterThisManyAttempts) + if (_currentNumberOfAttempts < SucceedAfterThisManyAttempts) return new HttpResponseMessage(HttpStatusCode.ServiceUnavailable); var authInfo = MAuthAuthenticator.GetAuthenticationInfo(request, mAuthCore); @@ -34,9 +53,17 @@ await mAuthCore.GetSignature(request, authInfo), return new HttpResponseMessage(HttpStatusCode.Unauthorized) { RequestMessage = request }; if (!request.RequestUri.AbsolutePath.Equals( - $"{Constants.MAuthTokenRequestPath}{clientUuid.ToHyphenString()}.json", + $"{Constants.MAuthTokenRequestPath}{_clientUuid.ToHyphenString()}.json", + StringComparison.OrdinalIgnoreCase) && + !request.RequestUri.AbsolutePath.Equals( + $"{Constants.MAuthTokenRequestPath}{_signingAppUuid.ToHyphenString()}.json", StringComparison.OrdinalIgnoreCase)) + { return new HttpResponseMessage(HttpStatusCode.NotFound) { RequestMessage = request }; + } + + bool isProtocolSuiteTest = request.RequestUri.AbsolutePath.Contains( + _signingAppUuid.ToHyphenString()); return new HttpResponseMessage(HttpStatusCode.OK) { @@ -46,10 +73,10 @@ await mAuthCore.GetSignature(request, authInfo), { security_token = new ApplicationInfo() { - Uuid = clientUuid, + Uuid = isProtocolSuiteTest ? _signingAppUuid : _clientUuid, Name = "Medidata.MAuth.Tests", CreationDate = new DateTimeOffset(2016, 8, 1, 0, 0, 0, TimeSpan.Zero), - PublicKey = TestExtensions.ClientPublicKey + PublicKey = isProtocolSuiteTest ? _signingPublicKey : TestExtensions.ClientPublicKey } }) ) diff --git a/tests/Medidata.MAuth.Tests/Infrastructure/TestExtensions.cs b/tests/Medidata.MAuth.Tests/Infrastructure/TestExtensions.cs index 960f2c3..f4a5253 100644 --- a/tests/Medidata.MAuth.Tests/Infrastructure/TestExtensions.cs +++ b/tests/Medidata.MAuth.Tests/Infrastructure/TestExtensions.cs @@ -20,13 +20,13 @@ internal static class TestExtensions public static readonly string ServerPrivateKey = GetKeyFromResource(nameof(ServerPrivateKey)).Result; public static readonly string ServerPublicKey = GetKeyFromResource(nameof(ServerPublicKey)).Result; - public static MAuthOptionsBase ServerOptions => new MAuthTestOptions() + public static MAuthOptionsBase ServerOptions(MAuthServerHandler serverHandler) => new MAuthTestOptions() { ApplicationUuid = ServerUuid, MAuthServiceUrl = TestUri, PrivateKey = ServerPrivateKey, MAuthServiceRetryPolicy = MAuthServiceRetryPolicy.RetryOnce, - MAuthServerHandler = new MAuthServerHandler() + MAuthServerHandler = serverHandler }; public static MAuthSigningOptions ClientOptions(DateTimeOffset signedTime) => new MAuthSigningOptions() @@ -37,18 +37,18 @@ internal static class TestExtensions }; public static MAuthOptionsBase GetServerOptionsWithAttempts(MAuthServiceRetryPolicy policy, - bool shouldSucceedWithin) => - new MAuthTestOptions() + bool shouldSucceedWithin, MAuthServerHandler serverHandler) + { + serverHandler.SucceedAfterThisManyAttempts = (int)policy + (shouldSucceedWithin ? 1 : 2); + return new MAuthTestOptions() { ApplicationUuid = ServerUuid, MAuthServiceUrl = TestUri, PrivateKey = ServerPrivateKey, MAuthServiceRetryPolicy = policy, - MAuthServerHandler = new MAuthServerHandler() - { - SucceedAfterThisManyAttempts = (int)policy + (shouldSucceedWithin ? 1 : 2) - } + MAuthServerHandler = serverHandler }; + } public static Task GetStringFromResource(string resourceName) { diff --git a/tests/Medidata.MAuth.Tests/MAuthAspNetCoreTests.cs b/tests/Medidata.MAuth.Tests/MAuthAspNetCoreTests.cs index 29bb642..26c2202 100644 --- a/tests/Medidata.MAuth.Tests/MAuthAspNetCoreTests.cs +++ b/tests/Medidata.MAuth.Tests/MAuthAspNetCoreTests.cs @@ -23,6 +23,7 @@ public async Task MAuthMiddleware_WithValidRequest_WillAuthenticate(string metho { // Arrange var testData = await method.FromResourceV2(); + var serverHandler = await MAuthServerHandler.CreateAsync(); using (var server = new TestServer(new WebHostBuilder().Configure(app => { @@ -31,7 +32,7 @@ public async Task MAuthMiddleware_WithValidRequest_WillAuthenticate(string metho options.ApplicationUuid = TestExtensions.ServerUuid; options.MAuthServiceUrl = TestExtensions.TestUri; options.PrivateKey = TestExtensions.ServerPrivateKey; - options.MAuthServerHandler = new MAuthServerHandler(); + options.MAuthServerHandler = serverHandler; options.HideExceptionsAndReturnUnauthorized = false; }); @@ -55,6 +56,7 @@ public async Task MAuthMiddleware_WithoutMAuthHeader_WillNotAuthenticate(string { // Arrange var testData = await method.FromResource(); + var serverHandler = await MAuthServerHandler.CreateAsync(); using (var server = new TestServer(new WebHostBuilder().Configure(app => { @@ -63,7 +65,7 @@ public async Task MAuthMiddleware_WithoutMAuthHeader_WillNotAuthenticate(string options.ApplicationUuid = TestExtensions.ServerUuid; options.MAuthServiceUrl = TestExtensions.TestUri; options.PrivateKey = TestExtensions.ServerPrivateKey; - options.MAuthServerHandler = new MAuthServerHandler(); + options.MAuthServerHandler = serverHandler; }); app.Run(async context => await new StreamWriter(context.Response.Body).WriteAsync("Done.")); @@ -87,6 +89,7 @@ public async Task MAuthMiddleware_WithEnabledExceptions_WillThrowException(strin { // Arrange var testData = await method.FromResource(); + var serverHandler = await MAuthServerHandler.CreateAsync(); using (var server = new TestServer(new WebHostBuilder().Configure(app => { @@ -95,7 +98,7 @@ public async Task MAuthMiddleware_WithEnabledExceptions_WillThrowException(strin options.ApplicationUuid = TestExtensions.ServerUuid; options.MAuthServiceUrl = TestExtensions.TestUri; options.PrivateKey = TestExtensions.ServerPrivateKey; - options.MAuthServerHandler = new MAuthServerHandler(); + options.MAuthServerHandler = serverHandler; options.HideExceptionsAndReturnUnauthorized = false; }); }))) @@ -121,6 +124,8 @@ public async Task MAuthMiddleware_WithNonSeekableBodyStream_WillRestoreBodyStrea var testData = await method.FromResourceV2(); var canSeek = false; var body = string.Empty; + var serverHandler = await MAuthServerHandler.CreateAsync(); + using (var server = new TestServer(new WebHostBuilder().Configure(app => { app @@ -130,7 +135,7 @@ public async Task MAuthMiddleware_WithNonSeekableBodyStream_WillRestoreBodyStrea options.ApplicationUuid = TestExtensions.ServerUuid; options.MAuthServiceUrl = TestExtensions.TestUri; options.PrivateKey = TestExtensions.ServerPrivateKey; - options.MAuthServerHandler = new MAuthServerHandler(); + options.MAuthServerHandler = serverHandler; }) .Run(async context => { diff --git a/tests/Medidata.MAuth.Tests/MAuthAuthenticatorTests.cs b/tests/Medidata.MAuth.Tests/MAuthAuthenticatorTests.cs index b112142..85f1cd7 100644 --- a/tests/Medidata.MAuth.Tests/MAuthAuthenticatorTests.cs +++ b/tests/Medidata.MAuth.Tests/MAuthAuthenticatorTests.cs @@ -44,7 +44,8 @@ public static async Task AuthenticateRequest_WithValidMWSRequest_WillAuthenticat { // Arrange var testData = await method.FromResource(); - var authenticator = new MAuthAuthenticator(TestExtensions.ServerOptions, NullLogger.Instance); + var serverHandler = await MAuthServerHandler.CreateAsync(); + var authenticator = new MAuthAuthenticator(TestExtensions.ServerOptions(serverHandler), NullLogger.Instance); var mAuthCore = new MAuthCore(); var signedRequest = await mAuthCore @@ -72,7 +73,8 @@ public static async Task AuthenticateRequest_WithValidMWSV2Request_WillAuthentic // Arrange var testData = await method.FromResourceV2(); var version = MAuthVersion.MWSV2; - var authenticator = new MAuthAuthenticator(TestExtensions.ServerOptions, NullLogger.Instance); + var serverHandler = await MAuthServerHandler.CreateAsync(); + var authenticator = new MAuthAuthenticator(TestExtensions.ServerOptions(serverHandler), NullLogger.Instance); var mAuthCore = new MAuthCoreV2(); var signedRequest = await mAuthCore @@ -101,9 +103,9 @@ public static async Task AuthenticateRequest_WithNumberOfAttempts_WillAuthentica { // Arrange var testData = await "GET".FromResourceV2(); - + var serverHandler = await MAuthServerHandler.CreateAsync(); var authenticator = new MAuthAuthenticator(TestExtensions.GetServerOptionsWithAttempts( - policy, shouldSucceedWithin: true), NullLogger.Instance); + policy, shouldSucceedWithin: true, serverHandler), NullLogger.Instance); var mAuthCore = new MAuthCoreV2(); var signedRequest = await mAuthCore @@ -132,8 +134,9 @@ public static async Task AuthenticateRequest_WithMWSV2Request_WithNumberOfAttemp // Arrange var testData = await "GET".FromResourceV2(); var version = MAuthVersion.MWSV2; + var serverHandler = await MAuthServerHandler.CreateAsync(); var authenticator = new MAuthAuthenticator(TestExtensions.GetServerOptionsWithAttempts( - policy, shouldSucceedWithin: true), NullLogger.Instance); + policy, shouldSucceedWithin: true, serverHandler), NullLogger.Instance); var mAuthCore = new MAuthCoreV2(); var signedRequest = await mAuthCore @@ -161,9 +164,9 @@ public static async Task AuthenticateRequest_AfterNumberOfAttempts_WillThrowExce { // Arrange var testData = await "GET".FromResource(); - + var serverHandler = await MAuthServerHandler.CreateAsync(); var authenticator = new MAuthAuthenticator(TestExtensions.GetServerOptionsWithAttempts( - policy, shouldSucceedWithin: false), NullLogger.Instance); + policy, shouldSucceedWithin: false, serverHandler), NullLogger.Instance); var mAuthCore = new MAuthCore(); var signedRequest = await mAuthCore @@ -197,8 +200,10 @@ public static async Task AuthenticateRequest_WithMWSV2Request_AfterNumberOfAttem // Arrange var testData = await "GET".FromResource(); var version = MAuthVersion.MWSV2; + var serverHandler = await MAuthServerHandler.CreateAsync(); + var authenticator = new MAuthAuthenticator(TestExtensions.GetServerOptionsWithAttempts( - policy, shouldSucceedWithin: false), NullLogger.Instance); + policy, shouldSucceedWithin: false, serverHandler), NullLogger.Instance); var mAuthCore = new MAuthCoreV2(); var signedRequest = await mAuthCore @@ -278,7 +283,7 @@ public static async Task AuthenticateRequest_WithMWSVersion_WithDisableV1_WillTh { // Arrange var testData = await method.FromResource(); - var testOptions = TestExtensions.ServerOptions; + var testOptions = TestExtensions.ServerOptions(await MAuthServerHandler.CreateAsync()); testOptions.DisableV1 = true; var authenticator = new MAuthAuthenticator(testOptions, NullLogger.Instance); var mAuthCore = new MAuthCore(); @@ -310,7 +315,7 @@ public static async Task GetAuthenticationInfo_WithSignedRequest_ForMWSV2Version // Arrange var testData = await method.FromResourceV2(); var version = MAuthVersion.MWSV2; - var testOptions = TestExtensions.ServerOptions; + var testOptions = TestExtensions.ServerOptions(await MAuthServerHandler.CreateAsync()); var mAuthCore = MAuthCoreFactory.Instantiate(version); // Act @@ -332,7 +337,7 @@ public static async Task GetAuthenticationInfo_WithSignedRequest_ForMWSVersion_W // Arrange var testData = await method.FromResource(); var version = MAuthVersion.MWS; - var testOptions = TestExtensions.ServerOptions; + var testOptions = TestExtensions.ServerOptions(await MAuthServerHandler.CreateAsync()); var mAuthCore = MAuthCoreFactory.Instantiate(version); // Act @@ -350,8 +355,8 @@ public static async Task AuthenticateRequest_WithDefaultRequest_WhenV2Fails_Fall // Arrange var testData = await "GET".FromResource(); var mockLogger = new Mock(); - - var authenticator = new MAuthAuthenticator(TestExtensions.ServerOptions, mockLogger.Object); + var serverHandler = await MAuthServerHandler.CreateAsync(); + var authenticator = new MAuthAuthenticator(TestExtensions.ServerOptions(serverHandler), mockLogger.Object); var requestData = testData.ToDefaultHttpRequestMessage(); // Act @@ -373,7 +378,7 @@ public static async Task AuthenticateRequest_WithDefaultRequest_AndDisableV1_Whe // Arrange var testData = await "GET".FromResource(); var mockLogger = new Mock(); - var testOptions = TestExtensions.ServerOptions; + var testOptions = TestExtensions.ServerOptions(await MAuthServerHandler.CreateAsync()); testOptions.DisableV1 = true; var authenticator = new MAuthAuthenticator(testOptions, mockLogger.Object); diff --git a/tests/Medidata.MAuth.Tests/MAuthOwinTests.cs b/tests/Medidata.MAuth.Tests/MAuthOwinTests.cs index 2b6fe97..5107d08 100644 --- a/tests/Medidata.MAuth.Tests/MAuthOwinTests.cs +++ b/tests/Medidata.MAuth.Tests/MAuthOwinTests.cs @@ -23,6 +23,7 @@ public static async Task MAuthMiddleware_WithValidRequest_WillAuthenticate(strin { // Arrange var testData = await method.FromResourceV2(); + var serverHandler = await MAuthServerHandler.CreateAsync(); using (var server = TestServer.Create(app => { @@ -31,7 +32,7 @@ public static async Task MAuthMiddleware_WithValidRequest_WillAuthenticate(strin options.ApplicationUuid = TestExtensions.ServerUuid; options.MAuthServiceUrl = TestExtensions.TestUri; options.PrivateKey = TestExtensions.ServerPrivateKey; - options.MAuthServerHandler = new MAuthServerHandler(); + options.MAuthServerHandler = serverHandler; }); app.Run(async context => await context.Response.WriteAsync("Done.")); @@ -54,6 +55,7 @@ public static async Task MAuthMiddleware_WithoutMAuthHeader_WillNotAuthenticate( { // Arrange var testData = await method.FromResource(); + var serverHandler = await MAuthServerHandler.CreateAsync(); using (var server = TestServer.Create(app => { @@ -62,7 +64,7 @@ public static async Task MAuthMiddleware_WithoutMAuthHeader_WillNotAuthenticate( options.ApplicationUuid = TestExtensions.ServerUuid; options.MAuthServiceUrl = TestExtensions.TestUri; options.PrivateKey = TestExtensions.ServerPrivateKey; - options.MAuthServerHandler = new MAuthServerHandler(); + options.MAuthServerHandler = serverHandler; }); app.Run(async context => await context.Response.WriteAsync("Done.")); @@ -86,6 +88,7 @@ public static async Task MAuthMiddleware_WithEnabledExceptions_WillThrowExceptio { // Arrange var testData = await method.FromResource(); + var serverHandler = await MAuthServerHandler.CreateAsync(); using (var server = TestServer.Create(app => { @@ -94,7 +97,7 @@ public static async Task MAuthMiddleware_WithEnabledExceptions_WillThrowExceptio options.ApplicationUuid = TestExtensions.ServerUuid; options.MAuthServiceUrl = TestExtensions.TestUri; options.PrivateKey = TestExtensions.ServerPrivateKey; - options.MAuthServerHandler = new MAuthServerHandler(); + options.MAuthServerHandler = serverHandler; options.HideExceptionsAndReturnUnauthorized = false; }); @@ -122,6 +125,7 @@ public static async Task MAuthMiddleware_WithNonSeekableBodyStream_WillRestoreBo var testData = await method.FromResourceV2(); var canSeek = false; var body = string.Empty; + var serverHandler = await MAuthServerHandler.CreateAsync(); using (var server = WebApp.Start("http://localhost:29999/", app => { @@ -130,7 +134,7 @@ public static async Task MAuthMiddleware_WithNonSeekableBodyStream_WillRestoreBo options.ApplicationUuid = TestExtensions.ServerUuid; options.MAuthServiceUrl = TestExtensions.TestUri; options.PrivateKey = TestExtensions.ServerPrivateKey; - options.MAuthServerHandler = new MAuthServerHandler(); + options.MAuthServerHandler = serverHandler; }); app.Run(async context => diff --git a/tests/Medidata.MAuth.Tests/MAuthProtocolSuiteTests.cs b/tests/Medidata.MAuth.Tests/MAuthProtocolSuiteTests.cs new file mode 100644 index 0000000..c11b8a6 --- /dev/null +++ b/tests/Medidata.MAuth.Tests/MAuthProtocolSuiteTests.cs @@ -0,0 +1,124 @@ +using Medidata.MAuth.Core; +using Medidata.MAuth.Core.Models; +using Medidata.MAuth.Tests.Infrastructure; +using Microsoft.Extensions.Logging.Abstractions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Medidata.MAuth.Tests.ProtocolTestSuite +{ + public class MAuthProtocolSuiteTests + { + private readonly ProtocolTestSuiteHelper _protcolTestHelper; + private readonly MAuthCoreV2 _mAuthCore; + + public MAuthProtocolSuiteTests() + { + _protcolTestHelper = new ProtocolTestSuiteHelper(); + _mAuthCore = new MAuthCoreV2(); + } + + [Theory, MemberData(nameof(TestCases))] + [Trait("Category","ProtocolTestSuite")] + public async Task MAuth_Execute_ProtocolTestSuite(string caseName) + { + // Arrange + var signConfig = await _protcolTestHelper.LoadSigningConfig(); + var requestData = await _protcolTestHelper.LoadUnsignedRequest(caseName); + var authz = await _protcolTestHelper.ReadAuthenticationHeader(caseName); + + var actual = new AssertSigningHandler(); + var clientOptions = ProtocolTestClientOptions( + signConfig.AppUuid, signConfig.PrivateKey, signConfig.RequestTime.FromUnixTimeSeconds()); + clientOptions.SignVersions = MAuthVersion.MWSV2; + var signingHandler = new MAuthSigningHandler(clientOptions, actual); + var request = ToHttpRequestMessage(requestData); + + var authInfo = new PrivateKeyAuthenticationInfo() + { + ApplicationUuid = signConfig.AppUuid, + SignedTime = signConfig.RequestTime.FromUnixTimeSeconds(), + PrivateKey = signConfig.PrivateKey + }; + + // Verify Signing and auth headers matches .authz file + // Act + using (var client = new HttpClient(signingHandler)) + { + await client.SendAsync(request); + } + + // Assert + Assert.Equal(authz.MccAuthentication, actual.MAuthHeaderV2); + Assert.Equal(authz.MccTime, long.Parse(actual.MAuthTimeHeaderV2)); + + if (!caseName.StartsWith("authentication-only")) + { + var sig = await _protcolTestHelper.ReadDigitalSignature(caseName); + + // Verify payload matches digital signature + // Act + var actualPayload = await _mAuthCore.CalculatePayload(request, authInfo); + + // Assert + Assert.Equal(sig, actualPayload); + + // Verify string_to_sign is matched + // Act + var result = await _mAuthCore.GetSignature(request, authInfo); + var sts = await _protcolTestHelper.ReadStringToSign(caseName); + + // Assert + Assert.Equal(sts, Encoding.UTF8.GetString(result)); + } + else + { + var serverOptions = TestExtensions.ServerOptions(await MAuthServerHandler.CreateAsync()); + var authenticator = new MAuthAuthenticator( + serverOptions, NullLogger.Instance); + + var signedRequest = await _mAuthCore + .AddAuthenticationInfo(request, new PrivateKeyAuthenticationInfo() + { + ApplicationUuid = signConfig.AppUuid, + PrivateKey = signConfig.PrivateKey, + SignedTime = signConfig.RequestTime.FromUnixTimeSeconds() + }); + + // Act + var isAuthenticated = await authenticator.AuthenticateRequest(signedRequest); + + // Assert + Assert.True(isAuthenticated); + } + } + + public static IEnumerable TestCases => + new ProtocolTestSuiteHelper().GetTestCases() + .Select(tc => new object[] { tc }); + + private static MAuthSigningOptions ProtocolTestClientOptions(Guid clientUuid, + string clientPrivateKey, DateTimeOffset signedTime) => new MAuthSigningOptions() + { + ApplicationUuid = clientUuid, + PrivateKey = clientPrivateKey, + SignedTime = signedTime + }; + + private static HttpRequestMessage ToHttpRequestMessage(UnSignedRequest data) + { + var result = new HttpRequestMessage(new HttpMethod(data.Verb), new Uri($"https://example.com{data.Url}")) + { + Content = !string.IsNullOrEmpty(data.Body) + ? new ByteArrayContent(Convert.FromBase64String(data.Body)) : null, + }; + + return result; + } + } +} diff --git a/tests/Medidata.MAuth.Tests/MAuthWebApiTests.cs b/tests/Medidata.MAuth.Tests/MAuthWebApiTests.cs index 3410897..bcb1fcf 100644 --- a/tests/Medidata.MAuth.Tests/MAuthWebApiTests.cs +++ b/tests/Medidata.MAuth.Tests/MAuthWebApiTests.cs @@ -21,13 +21,14 @@ public static async Task MAuthAuthenticatingHandler_WithValidMWSRequest_WillAuth // Arrange var testData = await method.FromResource(); var actual = new AssertSigningHandler(); + var serverHandler = await MAuthServerHandler.CreateAsync(); var handler = new MAuthAuthenticatingHandler(new MAuthWebApiOptions() { ApplicationUuid = TestExtensions.ServerUuid, MAuthServiceUrl = TestExtensions.TestUri, PrivateKey = TestExtensions.ServerPrivateKey, - MAuthServerHandler = new MAuthServerHandler() + MAuthServerHandler = serverHandler }, actual); using (var server = new HttpClient(handler)) @@ -53,12 +54,14 @@ public static async Task MAuthAuthenticatingHandler_WithValidMWSV2Request_WillAu var testData = await method.FromResourceV2(); var actual = new AssertSigningHandler(); var version = MAuthVersion.MWSV2; + var serverHandler = await MAuthServerHandler.CreateAsync(); + var handler = new MAuthAuthenticatingHandler(new MAuthWebApiOptions() { ApplicationUuid = TestExtensions.ServerUuid, MAuthServiceUrl = TestExtensions.TestUri, PrivateKey = TestExtensions.ServerPrivateKey, - MAuthServerHandler = new MAuthServerHandler() + MAuthServerHandler = serverHandler }, actual); using (var server = new HttpClient(handler)) @@ -82,13 +85,14 @@ public static async Task MAuthAuthenticatingHandler_WithoutMAuthHeader_WillNotAu { // Arrange var testData = await method.FromResource(); + var serverHandler = await MAuthServerHandler.CreateAsync(); var handler = new MAuthAuthenticatingHandler(new MAuthWebApiOptions() { ApplicationUuid = TestExtensions.ServerUuid, MAuthServiceUrl = TestExtensions.TestUri, PrivateKey = TestExtensions.ServerPrivateKey, - MAuthServerHandler = new MAuthServerHandler() + MAuthServerHandler = serverHandler }); using (var server = new HttpClient(handler)) @@ -111,13 +115,14 @@ public static async Task MAuthAuthenticatingHandler_WithEnabledExceptions_WillTh { // Arrange var testData = await method.FromResource(); + var serverHandler = await MAuthServerHandler.CreateAsync(); var handler = new MAuthAuthenticatingHandler(new MAuthWebApiOptions() { ApplicationUuid = TestExtensions.ServerUuid, MAuthServiceUrl = TestExtensions.TestUri, PrivateKey = TestExtensions.ServerPrivateKey, - MAuthServerHandler = new MAuthServerHandler(), + MAuthServerHandler = serverHandler, HideExceptionsAndReturnUnauthorized = false }); diff --git a/tests/Medidata.MAuth.Tests/ProtocolTestSuite/AuthenticationHeader.cs b/tests/Medidata.MAuth.Tests/ProtocolTestSuite/AuthenticationHeader.cs new file mode 100644 index 0000000..240e630 --- /dev/null +++ b/tests/Medidata.MAuth.Tests/ProtocolTestSuite/AuthenticationHeader.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace Medidata.MAuth.Tests.ProtocolTestSuite +{ + public class AuthenticationHeader + { + [JsonProperty(PropertyName = "MCC-Authentication")] + public string MccAuthentication { get; set; } + + [JsonProperty(PropertyName = "MCC-Time")] + public long MccTime { get; set; } + + } +} diff --git a/tests/Medidata.MAuth.Tests/ProtocolTestSuite/ProtocolTestSuiteHelper.cs b/tests/Medidata.MAuth.Tests/ProtocolTestSuite/ProtocolTestSuiteHelper.cs new file mode 100644 index 0000000..806f7a4 --- /dev/null +++ b/tests/Medidata.MAuth.Tests/ProtocolTestSuite/ProtocolTestSuiteHelper.cs @@ -0,0 +1,139 @@ +using Medidata.MAuth.Core; +using Newtonsoft.Json; +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Medidata.MAuth.Tests.ProtocolTestSuite +{ + public class ProtocolTestSuiteHelper + { + private string _testSuitePath; + private string _testCasePath; + + public ProtocolTestSuiteHelper() + { + var currentDirectory = Environment.CurrentDirectory; + _testSuitePath = Environment.GetEnvironmentVariable("TEST_SUITE_PATH") != null + ? Environment.GetEnvironmentVariable("TEST_SUITE_PATH") + : Path.GetFullPath(Path.Combine(currentDirectory, "../../../../../../mauth-protocol-test-suite")); + _testCasePath = Path.Combine(_testSuitePath, "protocols/MWSV2"); + } + + public async Task LoadSigningConfig() + { + var configFile = Path.Combine(_testSuitePath, "signing-config.json"); + + var signingConfig = await ReadSigningConfigParameters(configFile); + if (signingConfig is null )return null; + + signingConfig.PrivateKey = await GetPrivateKey(); + return signingConfig; + } + + public async Task ReadSignInAppUuid() + { + var signConfig = await LoadSigningConfig(); + return signConfig != null ? signConfig.AppUuid : default; + } + + public async Task GetPrivateKey() + { + var filePath = Path.Combine(_testSuitePath, "signing-params/rsa-key"); + return Encoding.UTF8.GetString(await ReadAsBytes(filePath)); + } + + public async Task GetPublicKey() + { + var publicKeyFilePath = Path.Combine(_testSuitePath, "signing-params/rsa-key-pub"); + + // TODO: remove this try catch when "mauth-protocol-test-suite" is added as submodule + try + { + return Encoding.UTF8.GetString(await ReadAsBytes(publicKeyFilePath)); + } + catch (DirectoryNotFoundException) + { + return null; + } + } + + public async Task LoadUnsignedRequest(string testCaseName) + { + var reqFilePath = Path.Combine(_testCasePath, testCaseName, $"{testCaseName}.req"); + var unsignedRequest = await ReadUnsignedRequest(reqFilePath); + unsignedRequest.Body = !string.IsNullOrEmpty(unsignedRequest.BodyFilePath) + ? await GetBinaryBody(testCaseName, unsignedRequest.BodyFilePath) + : !string.IsNullOrEmpty(unsignedRequest.Body) + ? Convert.ToBase64String(unsignedRequest.Body.ToBytes()) : unsignedRequest.Body; + + return unsignedRequest; + } + + public async Task GetBinaryBody(string caseName, string bodyFilepath) + { + if (string.IsNullOrEmpty(bodyFilepath)) + return null; + var completebodyFilePath = Path.Combine(_testCasePath, caseName, bodyFilepath); + var bytes = await ReadAsBytes(completebodyFilePath); + return Convert.ToBase64String(bytes); + } + + public string[] GetTestCases() + { + return Directory.Exists(_testCasePath) + ? Directory.GetDirectories(_testCasePath).Select(x => Path.GetFileName(x)).ToArray() + : null; + } + + public async Task ReadStringToSign(string testCaseName) + { + var stsFilePath = Path.Combine(_testCasePath, testCaseName, $"{testCaseName}.sts"); + var sts = Encoding.UTF8.GetString(await ReadAsBytes(stsFilePath)); + return sts.Replace("\r", ""); + } + + public async Task ReadDigitalSignature(string testCaseName) + { + var sigFilePath = Path.Combine(_testCasePath, testCaseName, $"{testCaseName}.sig"); + return Encoding.UTF8.GetString(await ReadAsBytes(sigFilePath)); + } + + public async Task ReadAuthenticationHeader(string testCaseName) + { + var authzFilePath = Path.Combine(_testCasePath, testCaseName, $"{testCaseName}.authz"); + return JsonConvert.DeserializeObject( + Encoding.UTF8.GetString(await ReadAsBytes(authzFilePath))); + } + + private async Task ReadUnsignedRequest(string requestPath) => + JsonConvert.DeserializeObject( + Encoding.UTF8.GetString(await ReadAsBytes(requestPath))); + + private async Task ReadSigningConfigParameters(string signingConfigJson) + { + // TODO: remove this try catch when "mauth-protocol-test-suite" is added as submodule + try + { + var signingConfigBytes = await ReadAsBytes(signingConfigJson); + return JsonConvert.DeserializeObject(Encoding.UTF8.GetString(signingConfigBytes)); + } + catch (DirectoryNotFoundException) + { + return null; + } + } + + private async Task ReadAsBytes(string filePath) + { + using (FileStream stream = File.OpenRead(filePath)) + { + byte[] result = new byte[stream.Length]; + await stream.ReadAsync(result, 0, (int)stream.Length); + return result; + } + } + } +} diff --git a/tests/Medidata.MAuth.Tests/ProtocolTestSuite/SigningConfig.cs b/tests/Medidata.MAuth.Tests/ProtocolTestSuite/SigningConfig.cs new file mode 100644 index 0000000..045bea1 --- /dev/null +++ b/tests/Medidata.MAuth.Tests/ProtocolTestSuite/SigningConfig.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; +using System; + +namespace Medidata.MAuth.Tests.ProtocolTestSuite +{ + public class SigningConfig + { + [JsonProperty(PropertyName ="app_uuid")] + public Guid AppUuid { get; set; } + + [JsonProperty(PropertyName = "request_time")] + public long RequestTime { get; set; } + + [JsonProperty(PropertyName = "private_key_file")] + public string PrivateKeyFile { get; set; } + + public string PrivateKey { get; set; } + } +} diff --git a/tests/Medidata.MAuth.Tests/ProtocolTestSuite/UnSignedRequest.cs b/tests/Medidata.MAuth.Tests/ProtocolTestSuite/UnSignedRequest.cs new file mode 100644 index 0000000..6351b95 --- /dev/null +++ b/tests/Medidata.MAuth.Tests/ProtocolTestSuite/UnSignedRequest.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; + +namespace Medidata.MAuth.Tests.ProtocolTestSuite +{ + public class UnSignedRequest + { + [JsonProperty(PropertyName = "verb")] + public string Verb { get; set; } + + [JsonProperty(PropertyName = "url")] + public string Url { get; set; } + + [JsonProperty(PropertyName = "body")] + public string Body { get; set; } + + [JsonProperty(PropertyName = "body_filepath")] + public string BodyFilePath { get; set; } + } +} diff --git a/tests/Medidata.MAuth.Tests/UtilityExtensionsTest.cs b/tests/Medidata.MAuth.Tests/UtilityExtensionsTest.cs index 8ac0ec0..e642858 100644 --- a/tests/Medidata.MAuth.Tests/UtilityExtensionsTest.cs +++ b/tests/Medidata.MAuth.Tests/UtilityExtensionsTest.cs @@ -68,7 +68,8 @@ public static async Task Authenticate_WithValidRequest_WillAuthenticate(string m }); // Act - var isAuthenticated = await signedRequest.Authenticate(TestExtensions.ServerOptions, NullLogger.Instance); + var isAuthenticated = await signedRequest.Authenticate( + TestExtensions.ServerOptions(await MAuthServerHandler.CreateAsync()), NullLogger.Instance); // Assert Assert.True(isAuthenticated);