diff --git a/CHANGELOG.md b/CHANGELOG.md index 336b97a..240267f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changes in Medidata.MAuth +## 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.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. - **[Core]** Fallback to V1 protocol when V2 athentication fails. 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/README.md b/README.md index 182bd2d..daf4c99 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 + // Enumerations of signing protocols, if not provided defaults to `MAuthVersion.MWS`for sign-in. + SignVersions = MAuthVersion.MWS | MAuthVersion.MWSV2 }); using (var client = new HttpClient(signingHandler)) @@ -125,10 +124,12 @@ 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`. -Signing with V2 protocol supports query string. +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 `MWS` protocol by default. + +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. @@ -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`. | +| **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 @@ -330,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 enumeration values of MAuth protcol versions `MWS` and/ or `MWSV2` protocol. +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? In this version we have added support for V2 protocol which uses `MCC-Authentication` as MAuthHeader and `MCC-Time` as 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.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/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/MAuthAuthenticator.cs b/src/Medidata.MAuth.Core/MAuthAuthenticator.cs index 9cb1bf7..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,46 +42,49 @@ public async Task AuthenticateRequest(HttpRequestMessage request) { try { - logger.LogInformation("Initiating Authentication of the request."); - var version = request.GetAuthHeaderValue().GetVersionFromAuthenticationHeader(); + _logger.LogInformation("Initiating Authentication of the request."); - if (options.DisableV1 && version == MAuthVersion.MWS) + 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); - if (!authenticated && version == MAuthVersion.MWSV2 && !options.DisableV1) + 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); - logger.LogWarning("Completed successful authentication attempt after fallback to V1"); + authenticated = await Authenticate(request, MAuthVersion.MWS, parsedHeader.Uuid).ConfigureAwait(false); + _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 @@ -89,11 +92,11 @@ 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}"; - logger.LogInformation(logMessage); + 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); var authInfo = GetAuthenticationInfo(request, mAuthCore); @@ -104,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); @@ -157,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.Core/MAuthCoreExtensions.cs b/src/Medidata.MAuth.Core/MAuthCoreExtensions.cs index 3a4f484..54f68d1 100644 --- a/src/Medidata.MAuth.Core/MAuthCoreExtensions.cs +++ b/src/Medidata.MAuth.Core/MAuthCoreExtensions.cs @@ -184,17 +184,53 @@ public static byte[] Concat(this byte[][] values) /// EncodedQueryParameter string. public static string BuildEncodedQueryParams(this string queryString) { - var encodedQueryStrings = new List(); + if (string.IsNullOrEmpty(queryString)) + return string.Empty; + var queryArray = queryString.Split('&'); - Array.Sort(queryArray, StringComparer.Ordinal); - Array.ForEach(queryArray, x => + var unescapedKeysAndValues = new KeyValuePair[queryArray.Length]; + + // unescaping + for (int i = 0; i < queryArray.Length; i++) { - 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 keyValue = queryArray[i].Split('='); + unescapedKeysAndValues[i] = new KeyValuePair( + Uri.UnescapeDataString(keyValue[0]), Uri.UnescapeDataString(keyValue[1])); + } + + // 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("&", escapedKeyValues).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 + var matches = Constants.LowerCaseHexPattern.Matches(path); + var normalizedPath = new StringBuilder(path); + foreach(var item in matches) + { + normalizedPath.Replace(item.ToString(), item.ToString().ToUpper()); + } + + // Replaces multiple slashes into single "/" + return Constants.SlashPattern.Replace(normalizedPath.ToString(), "/"); } } } diff --git a/src/Medidata.MAuth.Core/MAuthCoreV2.cs b/src/Medidata.MAuth.Core/MAuthCoreV2.cs index 8444b2c..bcec82b 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,10 @@ 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(1).BuildEncodedQueryParams().ToBytes() + : new byte[] { }; return new byte[][] { diff --git a/src/Medidata.MAuth.Core/MAuthSigningHandler.cs b/src/Medidata.MAuth.Core/MAuthSigningHandler.cs index dc679ee..0885a72 100644 --- a/src/Medidata.MAuth.Core/MAuthSigningHandler.cs +++ b/src/Medidata.MAuth.Core/MAuthSigningHandler.cs @@ -12,11 +12,10 @@ namespace Medidata.MAuth.Core /// public class MAuthSigningHandler: DelegatingHandler { - private readonly MAuthSigningOptions options; - private IMAuthCore mAuthCore; + 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 @@ -25,7 +24,7 @@ public class MAuthSigningHandler: DelegatingHandler /// The options for this message handler. public MAuthSigningHandler(MAuthSigningOptions options) { - this.options = options; + _options = options; } /// @@ -38,7 +37,7 @@ public MAuthSigningHandler(MAuthSigningOptions options) /// public MAuthSigningHandler(MAuthSigningOptions options, HttpMessageHandler innerHandler): base(innerHandler) { - this.options = options; + _options = options; } /// @@ -55,17 +54,15 @@ protected override async Task SendAsync( if (InnerHandler == null) InnerHandler = new HttpClientHandler(); - if (options.DisableV1 == false) // default + foreach (MAuthVersion version in Enum.GetValues(typeof(MAuthVersion))) { - // Add headers for V1 protocol as well - var mAuthCoreV1 = MAuthCoreFactory.Instantiate(MAuthVersion.MWS); - request = await mAuthCoreV1.Sign(request, options).ConfigureAwait(false); + if (_options.SignVersions.HasFlag(version)) + { + 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); diff --git a/src/Medidata.MAuth.Core/Models/MAuthVersion.cs b/src/Medidata.MAuth.Core/Models/MAuthVersion.cs index 05ad6be..45c3197 100644 --- a/src/Medidata.MAuth.Core/Models/MAuthVersion.cs +++ b/src/Medidata.MAuth.Core/Models/MAuthVersion.cs @@ -1,18 +1,21 @@ -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 { /// /// 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 82081d6..cc3c894 100644 --- a/src/Medidata.MAuth.Core/Options/MAuthSigningOptions.cs +++ b/src/Medidata.MAuth.Core/Options/MAuthSigningOptions.cs @@ -1,5 +1,6 @@ -using System; -using Medidata.MAuth.Core.Models; +using Medidata.MAuth.Core.Models; +using System; +using System.Collections.Generic; namespace Medidata.MAuth.Core { @@ -23,8 +24,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. + /// Enumeration values of MAuth protocol versions to sign requests, if not provided defaults to `MWS`. /// - public bool DisableV1 { get; set; } = false; + public MAuthVersion SignVersions { get; set; } = MAuthVersion.MWS; } } 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/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/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); } } } 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/MAuthCoreExtensionsTests.cs b/tests/Medidata.MAuth.Tests/MAuthCoreExtensionsTests.cs index daeaae1..c70ba9f 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,51 @@ 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()); + } + + [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) + { + var request = new Uri("http://localhost:2999" + input); + Assert.Equal(expected, request.AbsolutePath.NormalizeUriPath()); + } } } 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/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/MAuthSigningHandlerTests.cs b/tests/Medidata.MAuth.Tests/MAuthSigningHandlerTests.cs index 7d20d21..4012633 100644 --- a/tests/Medidata.MAuth.Tests/MAuthSigningHandlerTests.cs +++ b/tests/Medidata.MAuth.Tests/MAuthSigningHandlerTests.cs @@ -1,4 +1,5 @@ -using System.Net.Http; +using System.Collections.Generic; +using System.Net.Http; using System.Threading.Tasks; using Medidata.MAuth.Core; using Medidata.MAuth.Core.Models; @@ -14,25 +15,55 @@ 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_WithNoSignVersions_WillSignWithOnlyMWS(string method) { // Arrange var testData = await method.FromResource(); var actual = new AssertSigningHandler(); + var version = MAuthVersion.MWS; 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.Null(actual.MAuthHeaderV2); + Assert.Null(actual.MAuthTimeHeaderV2); + Assert.Equal(testData.MAuthHeader, actual.MAuthHeader); Assert.Equal(testData.SignedTime, long.Parse(actual.MAuthTimeHeader).FromUnixTimeSeconds()); + } - Assert.Equal(testData.MAuthHeaderV2, actual.MAuthHeaderV2); + [Theory] + [InlineData("GET")] + [InlineData("DELETE")] + [InlineData("POST")] + [InlineData("PUT")] + 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); + var version = MAuthVersion.MWS; + clientOptions.SignVersions = version; + var signingHandler = new MAuthSigningHandler(clientOptions, actual); + + // Act + using (var client = new HttpClient(signingHandler)) + { + 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.MAuthHeaderV2); + Assert.Null(actual.MAuthTimeHeaderV2); } [Theory] @@ -40,24 +71,26 @@ 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_WithSignVersionsMWSAndMWSV2_WillSignWithBothMWSAndMWSV2(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; - var signingHandler = new MAuthSigningHandler(clientOptions, actual); + clientOptions.SignVersions = MAuthVersion.MWS | MAuthVersion.MWSV2; + var signingHandler = new MAuthSigningHandler(clientOptions, actual); // Act using (var client = new HttpClient(signingHandler)) { - await client.SendAsync(testData.ToHttpRequestMessage(version)); + 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()); } 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); diff --git a/version.props b/version.props index 3891445..4db86b0 100644 --- a/version.props +++ b/version.props @@ -1,6 +1,6 @@  - 4.0.2 + 5.0.0