Skip to content

[MCC-732241] Fix caching with authentication when using utility extension method #71

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Feb 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*.user
*.userosscache
*.sln.docstates
.vscode/

# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
Expand Down Expand Up @@ -139,7 +140,7 @@ publish/
*.[Pp]ublish.xml
*.azurePubxml

# TODO: Un-comment the next line if you do not want to checkin
# TODO: Un-comment the next line if you do not want to checkin
# your web deploy settings because they may include unencrypted
# passwords
#*.pubxml
Expand Down
12 changes: 8 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
# Changes in Medidata.MAuth

## v5.1.1
- **[Core]** Fixed an issue with internal caching for the utility extension `Authenticate()` method.

## v5.1.0
- **[Core]** Added multi-target for .NET 5 to support synchronus HttpClient requests.
- **[Core]** Updated MAuthSigningHandler to sign synchronus requests.
- **[Core]** Added multi-target for .NET 5 to support synchronus HttpClient requests.
- **[Core]** Updated MAuthSigningHandler to sign synchronus requests.
- **[Core]** Updated MAuthAuthenticator to create and reuse a single HttpClient instead of creating a new one for each MAuthRequestRetrier.
- **[Core]** Updated MAuthAuthenticator to limit the calls to the cache item factory method to a single thread per key not already in the cache.

Expand Down Expand Up @@ -53,7 +57,7 @@ provided options.
- **[Core]** Removed constraint for the application uuids to be only version 4. Now the MAuth header validation won't throw error if the provided uuid is not version 4.

## v3.0.0
- **[All]** **Breaking** - Changed the HTTP status code response in case of any errors (including authentication and validation errors) from Forbidden (403) to Unauthorized (401).
- **[All]** **Breaking** - Changed the HTTP status code response in case of any errors (including authentication and validation errors) from Forbidden (403) to Unauthorized (401).
`HideExceptionsAndReturnForbidden` property of MAuth option class has also been renamed to `HideExceptionsAndReturnUnauthorized`.

## v2.4.1
Expand Down Expand Up @@ -94,7 +98,7 @@ downloads and referencing from NuGet
## v2.0.0
- **[Core]** The `MAuthSigningHandler` is accepting an `MAuthSigningOptions` instance instead of
an `MAuthOptions` instance (which in turn set to be an abstract class)
- [**Owin]** The OWIN middleware infrastructure provided request body stream gets replaced
- [**Owin]** The OWIN middleware infrastructure provided request body stream gets replaced
with a `MemoryStream` in cases when the original body stream is not seekable.

## v1.0.0
Expand Down
30 changes: 30 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>

<Project>
<Import Project="version.props"/>

<PropertyGroup>
<LangVersion>latest</LangVersion>
<Nullable>disable</Nullable>
<NoPackageAnalysis>true</NoPackageAnalysis>
<Copyright>Copyright © Medidata Solutions, Inc. $([System.DateTime]::Now.Year)</Copyright>
<Authors>Medidata Solutions</Authors>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/mdsol/mauth-client-dotnet</RepositoryUrl>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Company>Medidata Solutions, Inc.</Company>
<PackageProjectUrl>https://github.com/mdsol/mauth-client-dotnet</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/mdsol/mauth-client-dotnet/blob/master/LICENSE.md</PackageLicenseUrl>
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
<Product>Medidata Platform</Product>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
<IncludeSourceRevisionInInformationalVersion>true</IncludeSourceRevisionInInformationalVersion>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
</ItemGroup>
</Project>
22 changes: 0 additions & 22 deletions build/common.props

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\build\common.props" />

<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<TargetFramework>netstandard2.0</TargetFramework>
<Description>This package contains an ASP.NET Core middleware to validate signed http requests with the Medidata MAuth protocol. The middleware communicates with an MAuth server in order to confirm the validity of the request authentication header. Include this package in your ASP.NET Core web api if you want to authenticate the api requests signed with the MAuth protocol.</Description>
<AssemblyTitle>Medidata.MAuth.AspNetCore</AssemblyTitle>
<AssemblyName>Medidata.MAuth.AspNetCore</AssemblyName>
Expand Down
4 changes: 2 additions & 2 deletions src/Medidata.MAuth.Core/AsyncLazy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace Medidata.MAuth.Core
{
public class AsyncLazy<T> : Lazy<Task<T>>
internal class AsyncLazy<T> : Lazy<Task<T>>
{
public AsyncLazy(Func<T> valueFactory)
: base(() => Task.Factory.StartNew(valueFactory))
Expand All @@ -18,7 +18,7 @@ public AsyncLazy(Func<Task<T>> taskFactory)

public TaskAwaiter<T> GetAwaiter() => Value.GetAwaiter();

public ConfiguredTaskAwaitable<T> ConfigureAwait(bool continueOnCapturedContext)
public ConfiguredTaskAwaitable<T> ConfigureAwait(bool continueOnCapturedContext)
=> Value.ConfigureAwait(continueOnCapturedContext);
}
}
24 changes: 12 additions & 12 deletions src/Medidata.MAuth.Core/MAuthAuthenticator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,16 @@ namespace Medidata.MAuth.Core
{
internal class MAuthAuthenticator
{
private readonly IMemoryCache _cache;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't we declare this as

private static readonly IMemoryCache _cache = new MemoryCache(new MemoryCacheOptions());

here instead of in UtilityExtensions?
Having a variable in an extension method class feels a bit weird.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, that looks like a better solution! Thanks, let me update the code.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry to unresolve this, can we have a test for this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reverted back to my original solution because having the static cache in the MauthAuthenticator will screw up the unit tests.

private readonly MAuthOptionsBase _options;
private readonly IMemoryCache _cache = new MemoryCache(new MemoryCacheOptions());
private readonly ILogger _logger;
private readonly Lazy<HttpClient> _lazyHttpClient;

public Guid ApplicationUuid => _options.ApplicationUuid;

public MAuthAuthenticator(MAuthOptionsBase options, ILogger logger)
public MAuthAuthenticator(MAuthOptionsBase options, ILogger logger, IMemoryCache cache = null)
{
if (options.ApplicationUuid == default(Guid))
if (options.ApplicationUuid == default)
throw new ArgumentException(nameof(options.ApplicationUuid));

if (options.MAuthServiceUrl == null)
Expand All @@ -30,6 +30,7 @@ public MAuthAuthenticator(MAuthOptionsBase options, ILogger logger)
if (string.IsNullOrWhiteSpace(options.PrivateKey))
throw new ArgumentNullException(nameof(options.PrivateKey));

_cache = cache ?? new MemoryCache(new MemoryCacheOptions());
_options = options;
_logger = logger;
_lazyHttpClient = new Lazy<HttpClient>(() => CreateHttpClient(options));
Expand All @@ -48,16 +49,16 @@ public async Task<bool> AuthenticateRequest(HttpRequestMessage request)

var authHeader = request.GetAuthHeaderValue();
var version = authHeader.GetVersionFromAuthenticationHeader();
var parsedHeader = authHeader.ParseAuthenticationHeader();
var (Uuid, Base64Payload) = authHeader.ParseAuthenticationHeader();

if (_options.DisableV1 && version == MAuthVersion.MWS)
throw new InvalidVersionException($"Authentication with {version} version is disabled.");

var authenticated = await Authenticate(request, version, parsedHeader.Uuid).ConfigureAwait(false);
var authenticated = await Authenticate(request, version, Uuid).ConfigureAwait(false);
if (!authenticated && version == MAuthVersion.MWSV2 && !_options.DisableV1)
{
// fall back to V1 authentication
authenticated = await Authenticate(request, MAuthVersion.MWS, parsedHeader.Uuid).ConfigureAwait(false);
authenticated = await Authenticate(request, MAuthVersion.MWS, Uuid).ConfigureAwait(false);
_logger.LogWarning("Completed successful authentication attempt after fallback to V1");
}
return authenticated;
Expand Down Expand Up @@ -123,7 +124,7 @@ private AsyncLazy<ApplicationInfo> GetApplicationInfo(Guid applicationUuid) =>
_cache.GetOrCreateWithLock(
applicationUuid,
entry => new AsyncLazy<ApplicationInfo>(() => SendApplicationInfoRequest(entry, applicationUuid)));

private async Task<ApplicationInfo> SendApplicationInfoRequest(ICacheEntry entry, Guid applicationUuid)
{
var logMessage = "Mauth-client requesting from mAuth service application info not available " +
Expand Down Expand Up @@ -158,17 +159,17 @@ private async Task<ApplicationInfo> SendApplicationInfoRequest(ICacheEntry entry
/// <returns>The authentication information with the payload from the request.</returns>
internal static PayloadAuthenticationInfo GetAuthenticationInfo(HttpRequestMessage request, IMAuthCore mAuthCore)
{
var headerKeys = mAuthCore.GetHeaderKeys();
var authHeader = request.Headers.GetFirstValueOrDefault<string>(headerKeys.mAuthHeaderKey);
var (mAuthHeaderKey, mAuthTimeHeaderKey) = mAuthCore.GetHeaderKeys();
var authHeader = request.Headers.GetFirstValueOrDefault<string>(mAuthHeaderKey);

if (authHeader == null)
{
throw new ArgumentNullException(nameof(authHeader), "The MAuth header is missing from the request.");
}

var signedTime = request.Headers.GetFirstValueOrDefault<long>(headerKeys.mAuthTimeHeaderKey);
var signedTime = request.Headers.GetFirstValueOrDefault<long>(mAuthTimeHeaderKey);

if (signedTime == default(long))
if (signedTime == default)
{
throw new ArgumentException("Invalid MAuth signed time header value.", nameof(signedTime));
}
Expand Down Expand Up @@ -201,7 +202,6 @@ private HttpClient CreateHttpClient(MAuthOptionsBase options)
{
Timeout = TimeSpan.FromSeconds(options.AuthenticateRequestTimeoutSeconds)
};

}
}
}
10 changes: 5 additions & 5 deletions src/Medidata.MAuth.Core/MAuthCoreExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ namespace Medidata.MAuth.Core
internal static class MAuthCoreExtensions
{
/// <summary>
/// Deserializes an <see cref="ApplicationInfo"/> object from a content of a Http message.
/// Deserializes an <see cref="ApplicationInfo"/> object from a content of a Http message.
/// </summary>
/// <param name="content">The content to deserialize the information from.</param>
/// <returns>A Task object which will result the application information when it completes.</returns>
Expand All @@ -32,23 +32,23 @@ public static async Task<ApplicationInfo> FromResponse(this HttpContent content)
}

/// <summary>
/// Converts a <see cref="DateTimeOffset"/> value to a Unix equivalent time value.
/// Converts a <see cref="DateTimeOffset"/> value to a Unix equivalent time value.
/// </summary>
/// <param name="value">The date and time to be converted.</param>
/// <returns>The total seconds elapsed from the Unix epoch (1970-01-01 00:00:00).</returns>
public static long ToUnixTimeSeconds(this DateTimeOffset value) =>
(long)(value - Constants.UnixEpoch).TotalSeconds;

/// <summary>
/// Converts a <see cref="long"/> value that is a Unix time in seconds to a UTC <see cref="DateTimeOffset"/>.
/// Converts a <see cref="long"/> value that is a Unix time in seconds to a UTC <see cref="DateTimeOffset"/>.
/// </summary>
/// <param name="value">The Unix time seconds to be converted.</param>
/// <returns>The <see cref="DateTimeOffset"/> equivalent of the Unix time.</returns>
public static DateTimeOffset FromUnixTimeSeconds(this long value) =>
Constants.UnixEpoch.AddSeconds(value);

/// <summary>
/// Returns the hyphenated representation of a <see cref="Guid"/> as a <see cref="string"/>.
/// Returns the hyphenated representation of a <see cref="Guid"/> as a <see cref="string"/>.
/// </summary>
/// <param name="uuid">The <see cref="Guid"/> to convert to the hyphenated string.</param>
/// <returns>The uuid in a format with hyphens.</returns>
Expand All @@ -66,7 +66,7 @@ public static string ToHyphenString(this Guid uuid) =>
public static TValue GetFirstValueOrDefault<TValue>(this HttpHeaders headers, string key) =>
headers.TryGetValues(key, out IEnumerable<string> values) ?
(TValue)Convert.ChangeType(values.First(), typeof(TValue)) :
default(TValue);
default;

/// <summary>
/// Provides an SHA512 hash value of a string.
Expand Down
1 change: 0 additions & 1 deletion src/Medidata.MAuth.Core/Medidata.MAuth.Core.csproj
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\build\common.props" />

<PropertyGroup>
<Description>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.</Description>
Expand Down
7 changes: 5 additions & 2 deletions src/Medidata.MAuth.Core/UtilityExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Net.Http;
using System.Threading.Tasks;
using Medidata.MAuth.Core.Models;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;

Expand All @@ -12,6 +13,8 @@ namespace Medidata.MAuth.Core
/// </summary>
public static class UtilityExtensions
{
private static readonly IMemoryCache _cache = new MemoryCache(new MemoryCacheOptions());

/// <summary>
/// Parses an MAuth authentication HTTP header value for the application uuid and the MAuth signature payload.
/// If the parsing is not successful, an <see cref="ArgumentException"/> is thrown.
Expand Down Expand Up @@ -48,7 +51,7 @@ public static (Guid Uuid, string Base64Payload) ParseAuthenticationHeader(this s
public static bool TryParseAuthenticationHeader(this string headerValue,
out (Guid Uuid, string Base64Payload) result)
{
var match = headerValue.Contains("MWSV2") ? Constants.AuthenticationHeaderRegexV2.Match(headerValue) :
var match = headerValue.Contains("MWSV2") ? Constants.AuthenticationHeaderRegexV2.Match(headerValue) :
Constants.AuthenticationHeaderRegex.Match(headerValue);

result = default((Guid, string));
Expand All @@ -71,7 +74,7 @@ public static bool TryParseAuthenticationHeader(this string headerValue,
/// <returns>The task for the operation that is when completes will result in <see langword="true"/> if
/// the authentication is successful; otherwise <see langword="false"/>.</returns>
public static Task<bool> Authenticate(this HttpRequestMessage request, MAuthOptionsBase options, ILogger logger) =>
new MAuthAuthenticator(options, logger).AuthenticateRequest(request);
new MAuthAuthenticator(options, logger, _cache).AuthenticateRequest(request);

/// <summary>
/// Determines the MAuth version enumerator reading authHeader.
Expand Down
1 change: 0 additions & 1 deletion src/Medidata.MAuth.Owin/Medidata.MAuth.Owin.csproj
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\build\common.props" />

<PropertyGroup>
<Description>This package contains an OWIN middleware to validate signed http requests with the Medidata MAuth protocol. The middleware communicates with an MAuth server in order to confirm the validity of the request authentication header. Include this package in your OWIN-enabled web api if you want to authenticate the api requests signed with the MAuth protocol.</Description>
Expand Down
1 change: 0 additions & 1 deletion src/Medidata.MAuth.WebApi/Medidata.MAuth.WebApi.csproj
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\build\common.props" />

<PropertyGroup>
<Description>This package contains an HTTP message handler to validate signed http requests with the Medidata MAuth protocol. The handler communicates with an MAuth server in order to confirm the validity of the request authentication header. Include this package in your WebAPI application if you want to authenticate the api requests signed with the MAuth protocol.</Description>
Expand Down
7 changes: 3 additions & 4 deletions tests/Medidata.MAuth.Tests/Medidata.MAuth.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@

<PropertyGroup>
<Description>Assembly which contains unit tests of the Medidata.MAuth framework.</Description>
<Copyright>Copyright © Medidata Solutions, Inc. 2017</Copyright>
<AssemblyTitle>Medidata.MAuth.Tests</AssemblyTitle>
<Authors>Medidata Solutions, Inc.</Authors>
<TargetFrameworks>net461;netcoreapp2.1;net5.0</TargetFrameworks>
<AssemblyName>Medidata.MAuth.Tests</AssemblyName>
<PackageId>Medidata.MAuth.Tests</PackageId>
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
</PropertyGroup>

Expand Down Expand Up @@ -75,7 +74,7 @@
<Compile Remove="MAuthOwinTests.cs" />
<Compile Remove="MAuthWebApiTests.cs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.1.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.5.0" />
Expand Down
2 changes: 1 addition & 1 deletion version.props
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Project>
<PropertyGroup>
<Version>5.1.0</Version>
<Version>5.1.1</Version>
</PropertyGroup>
</Project>