From 27d90d4be8e405966bab88823b22191095a93a18 Mon Sep 17 00:00:00 2001 From: Evan Raffel Date: Tue, 21 Mar 2023 15:25:00 -0400 Subject: [PATCH 1/4] Add time constraint between signature and verification with 5 minute buffer. --- .../DateTimeOffsetWrapper.cs | 15 ++ .../IDateTimeOffsetWrapper.cs | 15 ++ src/Medidata.MAuth.Core/MAuthAuthenticator.cs | 19 +- .../Options/MAuthOptionsBase.cs | 5 + .../MAuthAspNetCoreTests.cs | 9 + .../MAuthAuthenticatorTests.cs | 192 +++++++++++++++++- .../MAuthProtocolSuiteTests.cs | 6 +- .../UtilityExtensionsTest.cs | 7 +- .../MAuthOwinTests.cs | 9 + .../MockDateTimeOffsetWrapper.cs | 12 ++ .../MAuthWebApiTests.cs | 17 +- 11 files changed, 286 insertions(+), 20 deletions(-) create mode 100644 src/Medidata.MAuth.Core/DateTimeOffsetWrapper.cs create mode 100644 src/Medidata.MAuth.Core/IDateTimeOffsetWrapper.cs create mode 100644 tests/Medidata.MAuth.Tests.Common/Infrastructure/MockDateTimeOffsetWrapper.cs diff --git a/src/Medidata.MAuth.Core/DateTimeOffsetWrapper.cs b/src/Medidata.MAuth.Core/DateTimeOffsetWrapper.cs new file mode 100644 index 0000000..783c11a --- /dev/null +++ b/src/Medidata.MAuth.Core/DateTimeOffsetWrapper.cs @@ -0,0 +1,15 @@ +using System; + +namespace Medidata.MAuth.Core +{ + internal class DateTimeOffsetWrapper : IDateTimeOffsetWrapper + { + /// + /// A facade around DatetimeOffset.UtcNow + /// + /// + /// The value of DateTimeOffset.UtcNow + /// + public DateTimeOffset GetUtcNow() => DateTimeOffset.UtcNow; + } +} diff --git a/src/Medidata.MAuth.Core/IDateTimeOffsetWrapper.cs b/src/Medidata.MAuth.Core/IDateTimeOffsetWrapper.cs new file mode 100644 index 0000000..2309064 --- /dev/null +++ b/src/Medidata.MAuth.Core/IDateTimeOffsetWrapper.cs @@ -0,0 +1,15 @@ +using System; + +namespace Medidata.MAuth.Core; + +/// +/// A facade for +/// +public interface IDateTimeOffsetWrapper +{ + /// + /// A facade for the UtcNow property. + /// + /// A value + DateTimeOffset GetUtcNow(); +} diff --git a/src/Medidata.MAuth.Core/MAuthAuthenticator.cs b/src/Medidata.MAuth.Core/MAuthAuthenticator.cs index fc1bf39..50feb21 100644 --- a/src/Medidata.MAuth.Core/MAuthAuthenticator.cs +++ b/src/Medidata.MAuth.Core/MAuthAuthenticator.cs @@ -13,9 +13,13 @@ namespace Medidata.MAuth.Core { internal class MAuthAuthenticator { + private const int AllowedDriftSeconds = 300; + private static readonly TimeSpan AllowedDriftTimeSpan = TimeSpan.FromSeconds(AllowedDriftSeconds); + private readonly ICacheService _cache; private readonly MAuthOptionsBase _options; private readonly ILogger _logger; + private readonly IDateTimeOffsetWrapper _dateTimeOffsetWrapper; private readonly Lazy _lazyHttpClient; public Guid ApplicationUuid => _options.ApplicationUuid; @@ -35,6 +39,7 @@ public MAuthAuthenticator(MAuthOptionsBase options, ILogger logger, ICacheServic _options = options; _logger = logger; _lazyHttpClient = new Lazy(() => CreateHttpClient(options)); + _dateTimeOffsetWrapper = options.DateTimeOffsetWrapper; } /// @@ -109,10 +114,22 @@ private async Task Authenticate(HttpRequestMessage request, MAuthVersion v authInfo.ApplicationUuid.ToString(), () => SendApplicationInfoRequest(authInfo.ApplicationUuid)).ConfigureAwait(false); + if (!IsSignatureTimeValid(authInfo.SignedTime)) + { + return false; + } + var signature = await mAuthCore.GetSignature(request, authInfo).ConfigureAwait(false); return mAuthCore.Verify(authInfo.Payload, signature, appInfo.PublicKey); + } - } + private bool IsSignatureTimeValid(DateTimeOffset signedTime) + { + var now = _dateTimeOffsetWrapper.GetUtcNow(); + var lowerBound = now - AllowedDriftTimeSpan; + var upperBound = now + AllowedDriftTimeSpan; + return signedTime >= lowerBound && signedTime <= upperBound; + } private async Task> SendApplicationInfoRequest(Guid applicationUuid) { diff --git a/src/Medidata.MAuth.Core/Options/MAuthOptionsBase.cs b/src/Medidata.MAuth.Core/Options/MAuthOptionsBase.cs index b1be0d6..f9217b8 100644 --- a/src/Medidata.MAuth.Core/Options/MAuthOptionsBase.cs +++ b/src/Medidata.MAuth.Core/Options/MAuthOptionsBase.cs @@ -50,5 +50,10 @@ public string PrivateKey /// Determines the boolean value if V1 option of signing should be disabled or not with default value of false. /// public bool DisableV1 { get; set; } = false; + + /// + /// Allow injection of a DateTimeOffset wrapper for testing purposes. + /// + public IDateTimeOffsetWrapper DateTimeOffsetWrapper { get; set; } = new DateTimeOffsetWrapper(); } } diff --git a/tests/Medidata.MAuth.AspNetCoreTests/MAuthAspNetCoreTests.cs b/tests/Medidata.MAuth.AspNetCoreTests/MAuthAspNetCoreTests.cs index 26c2202..4ddee4c 100644 --- a/tests/Medidata.MAuth.AspNetCoreTests/MAuthAspNetCoreTests.cs +++ b/tests/Medidata.MAuth.AspNetCoreTests/MAuthAspNetCoreTests.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Medidata.MAuth.AspNetCore; using Medidata.MAuth.Core; +using Medidata.MAuth.Tests.Common.Infrastructure; using Medidata.MAuth.Tests.Infrastructure; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -23,6 +24,7 @@ public async Task MAuthMiddleware_WithValidRequest_WillAuthenticate(string metho { // Arrange var testData = await method.FromResourceV2(); + var mockDateTimeOffsetWrapper = new MockDateTimeOffsetWrapper { MockedValue = testData.SignedTime }; var serverHandler = await MAuthServerHandler.CreateAsync(); using (var server = new TestServer(new WebHostBuilder().Configure(app => @@ -34,6 +36,7 @@ public async Task MAuthMiddleware_WithValidRequest_WillAuthenticate(string metho options.PrivateKey = TestExtensions.ServerPrivateKey; options.MAuthServerHandler = serverHandler; options.HideExceptionsAndReturnUnauthorized = false; + options.DateTimeOffsetWrapper = mockDateTimeOffsetWrapper; }); app.Run(async context => await new StreamWriter(context.Response.Body).WriteAsync("Done.")); @@ -56,6 +59,7 @@ public async Task MAuthMiddleware_WithoutMAuthHeader_WillNotAuthenticate(string { // Arrange var testData = await method.FromResource(); + var mockDateTimeOffsetWrapper = new MockDateTimeOffsetWrapper { MockedValue = testData.SignedTime }; var serverHandler = await MAuthServerHandler.CreateAsync(); using (var server = new TestServer(new WebHostBuilder().Configure(app => @@ -66,6 +70,7 @@ public async Task MAuthMiddleware_WithoutMAuthHeader_WillNotAuthenticate(string options.MAuthServiceUrl = TestExtensions.TestUri; options.PrivateKey = TestExtensions.ServerPrivateKey; options.MAuthServerHandler = serverHandler; + options.DateTimeOffsetWrapper = mockDateTimeOffsetWrapper; }); app.Run(async context => await new StreamWriter(context.Response.Body).WriteAsync("Done.")); @@ -89,6 +94,7 @@ public async Task MAuthMiddleware_WithEnabledExceptions_WillThrowException(strin { // Arrange var testData = await method.FromResource(); + var mockDateTimeOffsetWrapper = new MockDateTimeOffsetWrapper { MockedValue = testData.SignedTime }; var serverHandler = await MAuthServerHandler.CreateAsync(); using (var server = new TestServer(new WebHostBuilder().Configure(app => @@ -100,6 +106,7 @@ public async Task MAuthMiddleware_WithEnabledExceptions_WillThrowException(strin options.PrivateKey = TestExtensions.ServerPrivateKey; options.MAuthServerHandler = serverHandler; options.HideExceptionsAndReturnUnauthorized = false; + options.DateTimeOffsetWrapper = mockDateTimeOffsetWrapper; }); }))) { @@ -122,6 +129,7 @@ public async Task MAuthMiddleware_WithNonSeekableBodyStream_WillRestoreBodyStrea { // Arrange var testData = await method.FromResourceV2(); + var mockDateTimeOffsetWrapper = new MockDateTimeOffsetWrapper { MockedValue = testData.SignedTime }; var canSeek = false; var body = string.Empty; var serverHandler = await MAuthServerHandler.CreateAsync(); @@ -136,6 +144,7 @@ public async Task MAuthMiddleware_WithNonSeekableBodyStream_WillRestoreBodyStrea options.MAuthServiceUrl = TestExtensions.TestUri; options.PrivateKey = TestExtensions.ServerPrivateKey; options.MAuthServerHandler = serverHandler; + options.DateTimeOffsetWrapper = mockDateTimeOffsetWrapper; }) .Run(async context => { diff --git a/tests/Medidata.MAuth.CoreTests/MAuthAuthenticatorTests.cs b/tests/Medidata.MAuth.CoreTests/MAuthAuthenticatorTests.cs index 2e10adc..e8914b8 100644 --- a/tests/Medidata.MAuth.CoreTests/MAuthAuthenticatorTests.cs +++ b/tests/Medidata.MAuth.CoreTests/MAuthAuthenticatorTests.cs @@ -5,6 +5,7 @@ using Medidata.MAuth.Core; using Medidata.MAuth.Core.Exceptions; using Medidata.MAuth.Core.Models; +using Medidata.MAuth.Tests.Common.Infrastructure; using Medidata.MAuth.Tests.Infrastructure; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -43,8 +44,11 @@ public static async Task AuthenticateRequest_WithValidMWSRequest_WillAuthenticat { // Arrange var testData = await method.FromResource(); + var mockDateTimeOffsetWrapper = new MockDateTimeOffsetWrapper { MockedValue = testData.SignedTime }; var serverHandler = await MAuthServerHandler.CreateAsync(); - var authenticator = new MAuthAuthenticator(TestExtensions.ServerOptions(serverHandler), NullLogger.Instance); + var options = TestExtensions.ServerOptions(serverHandler); + options.DateTimeOffsetWrapper = mockDateTimeOffsetWrapper; + var authenticator = new MAuthAuthenticator(options, NullLogger.Instance); var mAuthCore = new MAuthCore(); var request = testData.ToHttpRequestMessage(MAuthVersion.MWS); @@ -75,9 +79,12 @@ public static async Task AuthenticateRequest_WithValidMWSV2Request_WillAuthentic { // Arrange var testData = await method.FromResourceV2(); + var mockDateTimeOffsetWrapper = new MockDateTimeOffsetWrapper { MockedValue = testData.SignedTime }; var version = MAuthVersion.MWSV2; var serverHandler = await MAuthServerHandler.CreateAsync(); - var authenticator = new MAuthAuthenticator(TestExtensions.ServerOptions(serverHandler), NullLogger.Instance); + var options = TestExtensions.ServerOptions(serverHandler); + options.DateTimeOffsetWrapper = mockDateTimeOffsetWrapper; + var authenticator = new MAuthAuthenticator(options, NullLogger.Instance); var mAuthCore = new MAuthCoreV2(); var authInfo = new PrivateKeyAuthenticationInfo() @@ -100,6 +107,152 @@ public static async Task AuthenticateRequest_WithValidMWSV2Request_WillAuthentic Assert.True(isAuthenticated); } + [Theory] + [InlineData("GET")] + [InlineData("DELETE")] + [InlineData("POST")] + [InlineData("PUT")] + public static async Task AuthenticateRequest_WithMWSRequestSignedTooEarly_WillFailToAuthenticate(string method) + { + // Arrange + var testData = await method.FromResource(); + var mockDateTimeOffsetWrapper = new MockDateTimeOffsetWrapper { MockedValue = testData.SignedTime.AddDays(1) }; + var serverHandler = await MAuthServerHandler.CreateAsync(); + var options = TestExtensions.ServerOptions(serverHandler); + options.DateTimeOffsetWrapper = mockDateTimeOffsetWrapper; + var authenticator = new MAuthAuthenticator(options, NullLogger.Instance); + var mAuthCore = new MAuthCore(); + + var request = testData.ToHttpRequestMessage(MAuthVersion.MWS); + var requestContents = await request.GetRequestContentAsBytesAsync(); + var authInfo = new PrivateKeyAuthenticationInfo() + { + ApplicationUuid = testData.ApplicationUuid, + PrivateKey = TestExtensions.ClientPrivateKey, + SignedTime = testData.SignedTime + }; + + var signedRequest = mAuthCore + .AddAuthenticationInfo(request, authInfo, requestContents); + + // Act + var isAuthenticated = await authenticator.AuthenticateRequest(signedRequest); + + // Assert + Assert.False(isAuthenticated); + } + + [Theory] + [InlineData("GET")] + [InlineData("DELETE")] + [InlineData("POST")] + [InlineData("PUT")] + public static async Task AuthenticateRequest_WithMWSRequestSignedTooLate_WillFailToAuthenticate(string method) + { + // Arrange + var testData = await method.FromResource(); + var mockDateTimeOffsetWrapper = new MockDateTimeOffsetWrapper { MockedValue = testData.SignedTime.AddDays(-1) }; + var serverHandler = await MAuthServerHandler.CreateAsync(); + var options = TestExtensions.ServerOptions(serverHandler); + options.DateTimeOffsetWrapper = mockDateTimeOffsetWrapper; + var authenticator = new MAuthAuthenticator(options, NullLogger.Instance); + var mAuthCore = new MAuthCore(); + + var request = testData.ToHttpRequestMessage(MAuthVersion.MWS); + var requestContents = await request.GetRequestContentAsBytesAsync(); + var authInfo = new PrivateKeyAuthenticationInfo() + { + ApplicationUuid = testData.ApplicationUuid, + PrivateKey = TestExtensions.ClientPrivateKey, + SignedTime = testData.SignedTime + }; + + var signedRequest = mAuthCore + .AddAuthenticationInfo(request, authInfo, requestContents); + + // Act + var isAuthenticated = await authenticator.AuthenticateRequest(signedRequest); + + // Assert + Assert.False(isAuthenticated); + } + + [Theory] + [InlineData("GET")] + [InlineData("DELETE")] + [InlineData("POST")] + [InlineData("PUT")] + public static async Task AuthenticateRequest_WithMWS2RequestSignedTooEarly_AndFallbackDisabled_WillFailToAuthenticate(string method) + { + // Arrange + var testData = await method.FromResourceV2(); + var mockDateTimeOffsetWrapper = new MockDateTimeOffsetWrapper { MockedValue = testData.SignedTime.AddDays(1) }; + var version = MAuthVersion.MWSV2; + var serverHandler = await MAuthServerHandler.CreateAsync(); + var options = TestExtensions.ServerOptions(serverHandler); + options.DateTimeOffsetWrapper = mockDateTimeOffsetWrapper; + options.DisableV1 = true; + var authenticator = new MAuthAuthenticator(options, NullLogger.Instance); + var mAuthCore = new MAuthCoreV2(); + + var authInfo = new PrivateKeyAuthenticationInfo() + { + ApplicationUuid = testData.ApplicationUuid, + PrivateKey = TestExtensions.ClientPrivateKey, + SignedTime = testData.SignedTime + }; + + var httpRequestMessage = testData.ToHttpRequestMessage(version); + + var requestContents = await httpRequestMessage.GetRequestContentAsBytesAsync(); + var signedRequest = mAuthCore + .AddAuthenticationInfo(httpRequestMessage, authInfo, requestContents); + + // Act + var isAuthenticated = await authenticator.AuthenticateRequest(signedRequest); + + // Assert + Assert.False(isAuthenticated); + } + + [Theory] + [InlineData("GET")] + [InlineData("DELETE")] + [InlineData("POST")] + [InlineData("PUT")] + public static async Task AuthenticateRequest_WithMWS2RequestSignedTooLate_AndFallbackDisabled_WillFailToAuthenticate(string method) + { + // Arrange + var testData = await method.FromResourceV2(); + var mockDateTimeOffsetWrapper = new MockDateTimeOffsetWrapper { MockedValue = testData.SignedTime.AddDays(-1) }; + var version = MAuthVersion.MWSV2; + var serverHandler = await MAuthServerHandler.CreateAsync(); + var options = TestExtensions.ServerOptions(serverHandler); + options.DateTimeOffsetWrapper = mockDateTimeOffsetWrapper; + options.DisableV1 = true; + var authenticator = new MAuthAuthenticator(options, NullLogger.Instance); + var mAuthCore = new MAuthCoreV2(); + + var authInfo = new PrivateKeyAuthenticationInfo() + { + ApplicationUuid = testData.ApplicationUuid, + PrivateKey = TestExtensions.ClientPrivateKey, + SignedTime = testData.SignedTime + }; + + var httpRequestMessage = testData.ToHttpRequestMessage(version); + + var requestContents = await httpRequestMessage.GetRequestContentAsBytesAsync(); + var signedRequest = mAuthCore + .AddAuthenticationInfo(httpRequestMessage, authInfo, requestContents); + + // Act + var isAuthenticated = await authenticator.AuthenticateRequest(signedRequest); + + // Assert + Assert.False(isAuthenticated); + } + [Theory] [InlineData(MAuthServiceRetryPolicy.NoRetry)] @@ -111,9 +264,11 @@ public static async Task AuthenticateRequest_WithNumberOfAttempts_WillAuthentica { // Arrange var testData = await "GET".FromResourceV2(); + var mockDateTimeOffsetWrapper = new MockDateTimeOffsetWrapper { MockedValue = testData.SignedTime }; var serverHandler = await MAuthServerHandler.CreateAsync(); - var authenticator = new MAuthAuthenticator(TestExtensions.GetServerOptionsWithAttempts( - policy, shouldSucceedWithin: true, serverHandler), NullLogger.Instance); + var options = TestExtensions.GetServerOptionsWithAttempts(policy, shouldSucceedWithin: true, serverHandler); + options.DateTimeOffsetWrapper = mockDateTimeOffsetWrapper; + var authenticator = new MAuthAuthenticator(options, NullLogger.Instance); var mAuthCore = new MAuthCoreV2(); var authInfo = new PrivateKeyAuthenticationInfo() @@ -147,10 +302,12 @@ public static async Task AuthenticateRequest_WithMWSV2Request_WithNumberOfAttemp { // Arrange var testData = await "GET".FromResourceV2(); + var mockDateTimeOffsetWrapper = new MockDateTimeOffsetWrapper { MockedValue = testData.SignedTime }; var version = MAuthVersion.MWSV2; var serverHandler = await MAuthServerHandler.CreateAsync(); - var authenticator = new MAuthAuthenticator(TestExtensions.GetServerOptionsWithAttempts( - policy, shouldSucceedWithin: true, serverHandler), NullLogger.Instance); + var options = TestExtensions.GetServerOptionsWithAttempts(policy, shouldSucceedWithin: true, serverHandler); + options.DateTimeOffsetWrapper = mockDateTimeOffsetWrapper; + var authenticator = new MAuthAuthenticator(options, NullLogger.Instance); var mAuthCore = new MAuthCoreV2(); var authInfo = new PrivateKeyAuthenticationInfo() @@ -183,9 +340,12 @@ public static async Task AuthenticateRequest_AfterNumberOfAttempts_WillThrowExce { // Arrange var testData = await "GET".FromResource(); + var mockDateTimeOffsetWrapper = new MockDateTimeOffsetWrapper { MockedValue = testData.SignedTime }; var serverHandler = await MAuthServerHandler.CreateAsync(); - var authenticator = new MAuthAuthenticator(TestExtensions.GetServerOptionsWithAttempts( - policy, shouldSucceedWithin: false, serverHandler), NullLogger.Instance); + var options = + TestExtensions.GetServerOptionsWithAttempts(policy, shouldSucceedWithin: false, serverHandler); + options.DateTimeOffsetWrapper = mockDateTimeOffsetWrapper; + var authenticator = new MAuthAuthenticator(options, NullLogger.Instance); var mAuthCore = new MAuthCore(); var authInfo = new PrivateKeyAuthenticationInfo() @@ -222,11 +382,14 @@ public static async Task AuthenticateRequest_WithMWSV2Request_AfterNumberOfAttem { // Arrange var testData = await "GET".FromResource(); + var mockDateTimeOffsetWrapper = new MockDateTimeOffsetWrapper { MockedValue = testData.SignedTime }; var version = MAuthVersion.MWSV2; var serverHandler = await MAuthServerHandler.CreateAsync(); - var authenticator = new MAuthAuthenticator(TestExtensions.GetServerOptionsWithAttempts( - policy, shouldSucceedWithin: false, serverHandler), NullLogger.Instance); + var options = + TestExtensions.GetServerOptionsWithAttempts(policy, shouldSucceedWithin: false, serverHandler); + options.DateTimeOffsetWrapper = mockDateTimeOffsetWrapper; + var authenticator = new MAuthAuthenticator(options, NullLogger.Instance); var mAuthCore = new MAuthCoreV2(); var authInfo = new PrivateKeyAuthenticationInfo() @@ -359,8 +522,10 @@ public static async Task AuthenticateRequest_WithMWSVersion_WithDisableV1_WillTh { // Arrange var testData = await method.FromResource(); + var mockDateTimeOffsetWrapper = new MockDateTimeOffsetWrapper { MockedValue = testData.SignedTime }; var testOptions = TestExtensions.ServerOptions(await MAuthServerHandler.CreateAsync()); testOptions.DisableV1 = true; + testOptions.DateTimeOffsetWrapper = mockDateTimeOffsetWrapper; var authenticator = new MAuthAuthenticator(testOptions, NullLogger.Instance); var mAuthCore = new MAuthCore(); @@ -435,9 +600,12 @@ public static async Task AuthenticateRequest_WithDefaultRequest_WhenV2Fails_Fall { // Arrange var testData = await "GET".FromResource(); + var mockDateTimeOffsetWrapper = new MockDateTimeOffsetWrapper { MockedValue = testData.SignedTime }; var mockLogger = new Mock(); var serverHandler = await MAuthServerHandler.CreateAsync(); - var authenticator = new MAuthAuthenticator(TestExtensions.ServerOptions(serverHandler), mockLogger.Object); + var options = TestExtensions.ServerOptions(serverHandler); + options.DateTimeOffsetWrapper = mockDateTimeOffsetWrapper; + var authenticator = new MAuthAuthenticator(options, mockLogger.Object); var requestData = testData.ToDefaultHttpRequestMessage(); // Act @@ -452,9 +620,11 @@ public static async Task AuthenticateRequest_WithDefaultRequest_AndDisableV1_Whe { // Arrange var testData = await "GET".FromResource(); + var mockDateTimeOffsetWrapper = new MockDateTimeOffsetWrapper { MockedValue = testData.SignedTime }; var mockLogger = new Mock(); var testOptions = TestExtensions.ServerOptions(await MAuthServerHandler.CreateAsync()); testOptions.DisableV1 = true; + testOptions.DateTimeOffsetWrapper = mockDateTimeOffsetWrapper; var authenticator = new MAuthAuthenticator(testOptions, mockLogger.Object); var requestData = testData.ToDefaultHttpRequestMessage(); diff --git a/tests/Medidata.MAuth.CoreTests/MAuthProtocolSuiteTests.cs b/tests/Medidata.MAuth.CoreTests/MAuthProtocolSuiteTests.cs index e8fe145..dde910c 100644 --- a/tests/Medidata.MAuth.CoreTests/MAuthProtocolSuiteTests.cs +++ b/tests/Medidata.MAuth.CoreTests/MAuthProtocolSuiteTests.cs @@ -8,6 +8,7 @@ using System.Net.Http; using System.Text; using System.Threading.Tasks; +using Medidata.MAuth.Tests.Common.Infrastructure; using Xunit; namespace Medidata.MAuth.Tests.ProtocolTestSuite @@ -80,9 +81,10 @@ public async Task MAuth_Execute_ProtocolTestSuite(string caseName) } else { + var mockDateTimeOffsetWrapper = new MockDateTimeOffsetWrapper { MockedValue = signConfig.RequestTime.FromUnixTimeSeconds() }; var serverOptions = TestExtensions.ServerOptions(await MAuthServerHandler.CreateAsync()); - var authenticator = new MAuthAuthenticator( - serverOptions, NullLogger.Instance); + serverOptions.DateTimeOffsetWrapper = mockDateTimeOffsetWrapper; + var authenticator = new MAuthAuthenticator(serverOptions, NullLogger.Instance); var privateKeyAuthenticationInfo = new PrivateKeyAuthenticationInfo() { diff --git a/tests/Medidata.MAuth.CoreTests/UtilityExtensionsTest.cs b/tests/Medidata.MAuth.CoreTests/UtilityExtensionsTest.cs index 68849b4..10004cc 100644 --- a/tests/Medidata.MAuth.CoreTests/UtilityExtensionsTest.cs +++ b/tests/Medidata.MAuth.CoreTests/UtilityExtensionsTest.cs @@ -1,6 +1,7 @@ using System; using System.Threading.Tasks; using Medidata.MAuth.Core; +using Medidata.MAuth.Tests.Common.Infrastructure; using Medidata.MAuth.Tests.Infrastructure; using Microsoft.Extensions.Logging.Abstractions; using Xunit; @@ -70,10 +71,12 @@ public static async Task Authenticate_WithValidRequest_WillAuthenticate(string m var requestContents = await request.GetRequestContentAsBytesAsync(); var signedRequest = mAuthCore .AddAuthenticationInfo(testData.ToDefaultHttpRequestMessage(), authInfo, requestContents); + var mockDateTimeOffsetWrapper = new MockDateTimeOffsetWrapper{ MockedValue = testData.SignedTime }; + var options = TestExtensions.ServerOptions(await MAuthServerHandler.CreateAsync()); + options.DateTimeOffsetWrapper = mockDateTimeOffsetWrapper; // Act - var isAuthenticated = await signedRequest.Authenticate( - TestExtensions.ServerOptions(await MAuthServerHandler.CreateAsync()), NullLogger.Instance); + var isAuthenticated = await signedRequest.Authenticate(options, NullLogger.Instance); // Assert Assert.True(isAuthenticated); diff --git a/tests/Medidata.MAuth.OwinTests/MAuthOwinTests.cs b/tests/Medidata.MAuth.OwinTests/MAuthOwinTests.cs index e53031f..af5d9dc 100644 --- a/tests/Medidata.MAuth.OwinTests/MAuthOwinTests.cs +++ b/tests/Medidata.MAuth.OwinTests/MAuthOwinTests.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Medidata.MAuth.Core; using Medidata.MAuth.Owin; +using Medidata.MAuth.Tests.Common.Infrastructure; using Medidata.MAuth.Tests.Infrastructure; using Microsoft.Owin.Hosting; using Microsoft.Owin.Testing; @@ -40,6 +41,7 @@ public static async Task MAuthMiddleware_WithValidRequest_WillAuthenticate(strin // Arrange var testData = await method.FromResourceV2(); var serverHandler = await MAuthServerHandler.CreateAsync(); + var mockDateTimeOffsetWrapper = new MockDateTimeOffsetWrapper { MockedValue = testData.SignedTime }; using (var server = TestServer.Create(app => { @@ -49,6 +51,7 @@ public static async Task MAuthMiddleware_WithValidRequest_WillAuthenticate(strin options.MAuthServiceUrl = TestExtensions.TestUri; options.PrivateKey = TestExtensions.ServerPrivateKey; options.MAuthServerHandler = serverHandler; + options.DateTimeOffsetWrapper = mockDateTimeOffsetWrapper; }); app.Run(async context => await context.Response.WriteAsync("Done.")); @@ -72,6 +75,7 @@ public static async Task MAuthMiddleware_WithoutMAuthHeader_WillNotAuthenticate( // Arrange var testData = await method.FromResource(); var serverHandler = await MAuthServerHandler.CreateAsync(); + var mockDateTimeOffsetWrapper = new MockDateTimeOffsetWrapper { MockedValue = testData.SignedTime }; using (var server = TestServer.Create(app => { @@ -81,6 +85,7 @@ public static async Task MAuthMiddleware_WithoutMAuthHeader_WillNotAuthenticate( options.MAuthServiceUrl = TestExtensions.TestUri; options.PrivateKey = TestExtensions.ServerPrivateKey; options.MAuthServerHandler = serverHandler; + options.DateTimeOffsetWrapper = mockDateTimeOffsetWrapper; }); app.Run(async context => await context.Response.WriteAsync("Done.")); @@ -105,6 +110,7 @@ public static async Task MAuthMiddleware_WithEnabledExceptions_WillThrowExceptio // Arrange var testData = await method.FromResource(); var serverHandler = await MAuthServerHandler.CreateAsync(); + var mockDateTimeOffsetWrapper = new MockDateTimeOffsetWrapper { MockedValue = testData.SignedTime }; using (var server = TestServer.Create(app => { @@ -115,6 +121,7 @@ public static async Task MAuthMiddleware_WithEnabledExceptions_WillThrowExceptio options.PrivateKey = TestExtensions.ServerPrivateKey; options.MAuthServerHandler = serverHandler; options.HideExceptionsAndReturnUnauthorized = false; + options.DateTimeOffsetWrapper = mockDateTimeOffsetWrapper; }); app.Run(async context => await context.Response.WriteAsync("Done.")); @@ -142,6 +149,7 @@ public static async Task MAuthMiddleware_WithNonSeekableBodyStream_WillRestoreBo var canSeek = false; var body = string.Empty; var serverHandler = await MAuthServerHandler.CreateAsync(); + var mockDateTimeOffsetWrapper = new MockDateTimeOffsetWrapper { MockedValue = testData.SignedTime }; using (var server = WebApp.Start("http://localhost:29999/", app => { @@ -151,6 +159,7 @@ public static async Task MAuthMiddleware_WithNonSeekableBodyStream_WillRestoreBo options.MAuthServiceUrl = TestExtensions.TestUri; options.PrivateKey = TestExtensions.ServerPrivateKey; options.MAuthServerHandler = serverHandler; + options.DateTimeOffsetWrapper = mockDateTimeOffsetWrapper; }); app.Run(async context => diff --git a/tests/Medidata.MAuth.Tests.Common/Infrastructure/MockDateTimeOffsetWrapper.cs b/tests/Medidata.MAuth.Tests.Common/Infrastructure/MockDateTimeOffsetWrapper.cs new file mode 100644 index 0000000..3eb464e --- /dev/null +++ b/tests/Medidata.MAuth.Tests.Common/Infrastructure/MockDateTimeOffsetWrapper.cs @@ -0,0 +1,12 @@ +using System; +using Medidata.MAuth.Core; + +namespace Medidata.MAuth.Tests.Common.Infrastructure +{ + public class MockDateTimeOffsetWrapper : IDateTimeOffsetWrapper + { + public DateTimeOffset MockedValue { get; set; } + + public DateTimeOffset GetUtcNow() => MockedValue; + } +} diff --git a/tests/Medidata.MAuth.WebApiTests/MAuthWebApiTests.cs b/tests/Medidata.MAuth.WebApiTests/MAuthWebApiTests.cs index 2c25550..9288280 100644 --- a/tests/Medidata.MAuth.WebApiTests/MAuthWebApiTests.cs +++ b/tests/Medidata.MAuth.WebApiTests/MAuthWebApiTests.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using Medidata.MAuth.Core; using Medidata.MAuth.Core.Models; +using Medidata.MAuth.Tests.Common.Infrastructure; using Medidata.MAuth.Tests.Infrastructure; using Medidata.MAuth.WebApi; using Xunit; @@ -38,13 +39,15 @@ public static async Task MAuthAuthenticatingHandler_WithValidMWSRequest_WillAuth var testData = await method.FromResource(); var actual = new AssertSigningHandler(); var serverHandler = await MAuthServerHandler.CreateAsync(); + var mockDateTimeOffsetWrapper = new MockDateTimeOffsetWrapper { MockedValue = testData.SignedTime }; var handler = new MAuthAuthenticatingHandler(new MAuthWebApiOptions() { ApplicationUuid = TestExtensions.ServerUuid, MAuthServiceUrl = TestExtensions.TestUri, PrivateKey = TestExtensions.ServerPrivateKey, - MAuthServerHandler = serverHandler + MAuthServerHandler = serverHandler, + DateTimeOffsetWrapper = mockDateTimeOffsetWrapper, }, actual); using (var server = new HttpClient(handler)) @@ -71,13 +74,15 @@ public static async Task MAuthAuthenticatingHandler_WithValidMWSV2Request_WillAu var actual = new AssertSigningHandler(); var version = MAuthVersion.MWSV2; var serverHandler = await MAuthServerHandler.CreateAsync(); + var mockDateTimeOffsetWrapper = new MockDateTimeOffsetWrapper { MockedValue = testData.SignedTime }; var handler = new MAuthAuthenticatingHandler(new MAuthWebApiOptions() { ApplicationUuid = TestExtensions.ServerUuid, MAuthServiceUrl = TestExtensions.TestUri, PrivateKey = TestExtensions.ServerPrivateKey, - MAuthServerHandler = serverHandler + MAuthServerHandler = serverHandler, + DateTimeOffsetWrapper = mockDateTimeOffsetWrapper, }, actual); using (var server = new HttpClient(handler)) @@ -102,13 +107,15 @@ public static async Task MAuthAuthenticatingHandler_WithoutMAuthHeader_WillNotAu // Arrange var testData = await method.FromResource(); var serverHandler = await MAuthServerHandler.CreateAsync(); + var mockDateTimeOffsetWrapper = new MockDateTimeOffsetWrapper { MockedValue = testData.SignedTime }; var handler = new MAuthAuthenticatingHandler(new MAuthWebApiOptions() { ApplicationUuid = TestExtensions.ServerUuid, MAuthServiceUrl = TestExtensions.TestUri, PrivateKey = TestExtensions.ServerPrivateKey, - MAuthServerHandler = serverHandler + MAuthServerHandler = serverHandler, + DateTimeOffsetWrapper = mockDateTimeOffsetWrapper, }); using (var server = new HttpClient(handler)) @@ -132,6 +139,7 @@ public static async Task MAuthAuthenticatingHandler_WithEnabledExceptions_WillTh // Arrange var testData = await method.FromResource(); var serverHandler = await MAuthServerHandler.CreateAsync(); + var mockDateTimeOffsetWrapper = new MockDateTimeOffsetWrapper { MockedValue = testData.SignedTime }; var handler = new MAuthAuthenticatingHandler(new MAuthWebApiOptions() { @@ -139,7 +147,8 @@ public static async Task MAuthAuthenticatingHandler_WithEnabledExceptions_WillTh MAuthServiceUrl = TestExtensions.TestUri, PrivateKey = TestExtensions.ServerPrivateKey, MAuthServerHandler = serverHandler, - HideExceptionsAndReturnUnauthorized = false + HideExceptionsAndReturnUnauthorized = false, + DateTimeOffsetWrapper = mockDateTimeOffsetWrapper, }); using (var server = new HttpClient(handler)) From 25c8b89eb9fa5dee193b7b150623d9405e8b2c8b Mon Sep 17 00:00:00 2001 From: Evan Raffel Date: Tue, 21 Mar 2023 15:42:56 -0400 Subject: [PATCH 2/4] Bump to .net 6 --- .github/workflows/{test-net50.yaml => test-net60.yaml} | 6 +++--- src/Medidata.MAuth.Core/Medidata.MAuth.Core.csproj | 8 ++++---- .../Medidata.MAuth.AspNetCoreTests.csproj | 6 ++---- .../Medidata.MAuth.CoreTests.csproj | 2 +- 4 files changed, 10 insertions(+), 12 deletions(-) rename .github/workflows/{test-net50.yaml => test-net60.yaml} (79%) diff --git a/.github/workflows/test-net50.yaml b/.github/workflows/test-net60.yaml similarity index 79% rename from .github/workflows/test-net50.yaml rename to .github/workflows/test-net60.yaml index 553f3a8..1d8d5fa 100644 --- a/.github/workflows/test-net50.yaml +++ b/.github/workflows/test-net60.yaml @@ -1,4 +1,4 @@ -name: Build and Test (.NET 5.0) +name: Build and Test (.NET 6.0) on: [push] jobs: Test: @@ -9,6 +9,6 @@ jobs: with: submodules: 'recursive' - name: Run the Core tests - run: dotnet test $GITHUB_WORKSPACE/tests/Medidata.MAuth.CoreTests --framework net5.0 + run: dotnet test $GITHUB_WORKSPACE/tests/Medidata.MAuth.CoreTests --framework net6.0 - name: Run the ASP.NET Core tests - run: dotnet test $GITHUB_WORKSPACE/tests/Medidata.MAuth.AspNetCoreTests --framework net5.0 + run: dotnet test $GITHUB_WORKSPACE/tests/Medidata.MAuth.AspNetCoreTests --framework net6.0 diff --git a/src/Medidata.MAuth.Core/Medidata.MAuth.Core.csproj b/src/Medidata.MAuth.Core/Medidata.MAuth.Core.csproj index 95e231b..3b90f45 100644 --- a/src/Medidata.MAuth.Core/Medidata.MAuth.Core.csproj +++ b/src/Medidata.MAuth.Core/Medidata.MAuth.Core.csproj @@ -3,7 +3,7 @@ A core package for Medidata HMAC protocol implementation. This package contains the core functionality which used by the MAuth authentication protocol-specific components. This package also can be used standalone if you want to sign HTTP/HTTPS requests with Medidata MAuth keys using the .NET HttpClient message handler mechanism. Medidata.MAuth.Core - netstandard2.0;net5.0 + netstandard2.0;net6.0 Medidata.MAuth.Core medidata;mauth;hmac;authentication;core;httpclient;messagehandler @@ -12,11 +12,11 @@ all - - + + - + diff --git a/tests/Medidata.MAuth.AspNetCoreTests/Medidata.MAuth.AspNetCoreTests.csproj b/tests/Medidata.MAuth.AspNetCoreTests/Medidata.MAuth.AspNetCoreTests.csproj index 29e2f13..ef9a7ac 100644 --- a/tests/Medidata.MAuth.AspNetCoreTests/Medidata.MAuth.AspNetCoreTests.csproj +++ b/tests/Medidata.MAuth.AspNetCoreTests/Medidata.MAuth.AspNetCoreTests.csproj @@ -1,14 +1,12 @@  - net5.0 + net6.0 Unit tests for the Medidata.MAuth.AspNetCore package. - - - + diff --git a/tests/Medidata.MAuth.CoreTests/Medidata.MAuth.CoreTests.csproj b/tests/Medidata.MAuth.CoreTests/Medidata.MAuth.CoreTests.csproj index 05ae11d..100fc7f 100644 --- a/tests/Medidata.MAuth.CoreTests/Medidata.MAuth.CoreTests.csproj +++ b/tests/Medidata.MAuth.CoreTests/Medidata.MAuth.CoreTests.csproj @@ -1,7 +1,7 @@  - net5.0 + net6.0 Unit tests for the Medidata.MAuth.Core package. From 9ac0976695bf4802e41d9f271211381e259a5456 Mon Sep 17 00:00:00 2001 From: Evan Raffel Date: Tue, 21 Mar 2023 16:24:14 -0400 Subject: [PATCH 3/4] Refactor --- src/Medidata.MAuth.Core/MAuthAuthenticator.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Medidata.MAuth.Core/MAuthAuthenticator.cs b/src/Medidata.MAuth.Core/MAuthAuthenticator.cs index 50feb21..f55f65f 100644 --- a/src/Medidata.MAuth.Core/MAuthAuthenticator.cs +++ b/src/Medidata.MAuth.Core/MAuthAuthenticator.cs @@ -109,16 +109,16 @@ private async Task Authenticate(HttpRequestMessage request, MAuthVersion v var mAuthCore = MAuthCoreFactory.Instantiate(version); var authInfo = GetAuthenticationInfo(request, mAuthCore); - - var appInfo = await _cache.GetOrCreateWithLock( - authInfo.ApplicationUuid.ToString(), - () => SendApplicationInfoRequest(authInfo.ApplicationUuid)).ConfigureAwait(false); if (!IsSignatureTimeValid(authInfo.SignedTime)) { return false; } + var appInfo = await _cache.GetOrCreateWithLock( + authInfo.ApplicationUuid.ToString(), + () => SendApplicationInfoRequest(authInfo.ApplicationUuid)).ConfigureAwait(false); + var signature = await mAuthCore.GetSignature(request, authInfo).ConfigureAwait(false); return mAuthCore.Verify(authInfo.Payload, signature, appInfo.PublicKey); } From bf7b84829178af0a6d14df0fe5ed18f5518c751d Mon Sep 17 00:00:00 2001 From: Evan Raffel Date: Wed, 22 Mar 2023 19:27:04 -0400 Subject: [PATCH 4/4] Add logging --- src/Medidata.MAuth.Core/MAuthAuthenticator.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Medidata.MAuth.Core/MAuthAuthenticator.cs b/src/Medidata.MAuth.Core/MAuthAuthenticator.cs index f55f65f..200fab4 100644 --- a/src/Medidata.MAuth.Core/MAuthAuthenticator.cs +++ b/src/Medidata.MAuth.Core/MAuthAuthenticator.cs @@ -128,7 +128,18 @@ private bool IsSignatureTimeValid(DateTimeOffset signedTime) var now = _dateTimeOffsetWrapper.GetUtcNow(); var lowerBound = now - AllowedDriftTimeSpan; var upperBound = now + AllowedDriftTimeSpan; - return signedTime >= lowerBound && signedTime <= upperBound; + var isValid = signedTime >= lowerBound && signedTime <= upperBound; + + if (!isValid) + { + _logger.LogInformation( + "Time verification failed. {signedTime} is not within {AllowedDriftSeconds} seconds of #{now}", + signedTime, + AllowedDriftSeconds, + now); + } + + return isValid; } private async Task> SendApplicationInfoRequest(Guid applicationUuid)