Skip to content

Commit

Permalink
Merge pull request #14 from mdsol/develop
Browse files Browse the repository at this point in the history
Release of v2.2.0
  • Loading branch information
bvillanueva-mdsol authored Mar 1, 2017
2 parents 45f4c3b + aa657c7 commit 4367b95
Show file tree
Hide file tree
Showing 17 changed files with 278 additions and 55 deletions.
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
# Changes in Medidata.MAuth

## v2.1.2
## v2.2.0
- **[Medidata.MAuth.Core]** Decreased the **default** timeout from 10 seconds to 3 seconds for the MAuth service
requests in order to decrease the chance of service request congestion (the timeout still configurable in the options)
- **[Medidata.MAuth.Core]** Added a new feature to make multiple attempts to communicate with the MAuth service in case
there are unsuccessful responses. The number of attempts (i.e. retry policy) is configurable through the options
(`MAuthServiceRetryPolicy`)
- **[Medidata.MAuth.Core]** Fixed the .NET Framework assemblies being referenced as dependencies instead of
framework assemblies causing unnecessary package downloads and referencing from NuGet
- **[All]** Updated copyright year numbers to the current (2017) year
Expand Down
16 changes: 13 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,8 @@ public class Startup
{
options.ApplicationUuid = new Guid("a419de8f-d759-4db9-b9a7-c2cd14174987");
options.MAuthServiceUrl = new Uri("https://mauth.imedidata.com");
options.AuthenticateRequestTimeoutSeconds = 10;
options.AuthenticateRequestTimeoutSeconds = 3;
options.MAuthServiceRetryPolicy = MAuthServiceRetryPolicy.RetryOnce;
options.HideExceptionsAndReturnForbidden = true;
options.PrivateKey = File.ReadAllText("ServerPrivateKey.pem");
options.Bypass = (request) => request.Uri.AbsolutePath.StartsWith("/allowed");
Expand All @@ -146,7 +147,8 @@ The middleware takes an `MAuthMiddlewareOptions` instance to set up the authenti
| **ApplicationUuid** | Determines the unique identifier of the server application used for the MAuth service authentication requests. This uuid needs to be registered with the MAuth Server in order to use it. |
| **MAuthServiceUrl** | Determines the endpoint of the MAuth authentication service. This endpoint is used by the authentication process to verify the validity of the signed request. |
| **PrivateKey** | Determines the RSA private key of the server application for the authentication requests. This key must be in a PEM ASN.1 format. |
| **AuthenticateRequestTimeoutSeconds** | An optional parameter that determines the timeout in seconds for the MAuth authentication request - the MAuth component will try to reach the MAuth server for this duration before it throws an exception. If not specified, the default value will be **10 seconds**. |
| **AuthenticateRequestTimeoutSeconds** | An optional parameter that determines the timeout in seconds for the MAuth authentication request - the MAuth component will try to reach the MAuth server for this duration before it throws an exception. If not specified, the default value will be **3 seconds**. |
| **MAuthServiceRetryPolicy** | The policy for the retry attempts when communicating with the MAuth service. The following policies can be used: `NoRetry` (no retries), `RetryOnce` (one additional attempt), `RetryTwice` (two additional attempts) and `Agressive` (9 additional attempts) - the default value is **RetryOnce**. |
| **HideExceptionsAndReturnForbidden** | An optional parameter that determines if the middleware should swallow all exceptions and return an empty HTTP response with a status code Forbidden (403) in case of any errors (including authentication and validation errors). The default is **true**. |
| **Bypass** | Determines a function which evaluates if a given request should bypass the MAuth authentication. |

Expand Down Expand Up @@ -184,7 +186,8 @@ public static class WebApiConfig
{
ApplicationUuid = new Guid("a419de8f-d759-4db9-b9a7-c2cd14174987"),
MAuthServiceUrl = new Uri("https://mauth.imedidata.com"),
AuthenticateRequestTimeoutSeconds = 10,
AuthenticateRequestTimeoutSeconds = 3,
MAuthServiceRetryPolicy = MAuthServiceRetryPolicy.RetryOnce,
HideExceptionsAndReturnForbidden = true,
PrivateKey = File.ReadAllText("ServerPrivateKey.pem")
};
Expand Down Expand Up @@ -262,4 +265,11 @@ utilizes the
[Windows OS built-in WinINET caching](https://msdn.microsoft.com/en-us/library/windows/desktop/aa383928(v=vs.85).aspx),
thus it respects all the HTTP-specific cache headers provided by the MAuth server.

##### The documentation for the `MAuthServiceRetryPolicy.Agressive` retry policy says that it is not recommended for production use. What is the reason for this?

This policy will make the number of requests to the MAuth service to an overall 10 attempts. We believe that the chance
to receive a successful response from the MAuth service is gradually decreasing by the number of attempts (the more
the clients are sending requests to a presumably overloaded server the less the chance for a successful response) -
therefore we do not recommend to use this policy in any production scenario.


36 changes: 36 additions & 0 deletions src/Medidata.MAuth.Core/Exceptions/RetriedRequestException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.Net.Http;

namespace Medidata.MAuth.Core
{
/// <summary>
/// The exception that is thrown when a number of request attempts to the MAuth server fails.
/// </summary>
public class RetriedRequestException: Exception
{
/// <summary>
/// Determines the responses for each request attempts made.
/// </summary>
public ICollection<HttpResponseMessage> Responses { get; } = new List<HttpResponseMessage>();

/// <summary>
/// Determines the request message of the first request attempt to the MAuth server.
/// </summary>
public HttpRequestMessage Request { get; set; }

/// <summary>
/// Initializes a new instance of the <see cref="RetriedRequestException"/> class with the specified message.
/// </summary>
/// <param name="message">A message that describes the request failure.</param>
public RetriedRequestException(string message): base(message) { }

/// <summary>
/// Initializes a new instance of the <see cref="RetriedRequestException"/> class with the specified message
/// and inner exception.
/// </summary>
/// <param name="message">A message that describes the request failure.</param>
/// <param name="innerException">An exception that is the cause of the current exception.</param>
public RetriedRequestException(string message, Exception innerException): base(message, innerException) { }
}
}
52 changes: 13 additions & 39 deletions src/Medidata.MAuth.Core/MAuthAuthenticator.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
using System;
#if !NETSTANDARD1_4
using System.Net.Cache;
#endif
using System.Net.Http;
using System.Threading.Tasks;
using Org.BouncyCastle.Crypto;
Expand All @@ -11,6 +8,7 @@ namespace Medidata.MAuth.Core
internal class MAuthAuthenticator
{
private readonly MAuthOptionsBase options;
private MAuthRequestRetrier retrier;

public Guid ApplicationUuid => options.ApplicationUuid;

Expand All @@ -26,6 +24,8 @@ public MAuthAuthenticator(MAuthOptionsBase options)
throw new ArgumentNullException(nameof(options.PrivateKey));

this.options = options;

retrier = new MAuthRequestRetrier(options);
}

public async Task<bool> AuthenticateRequest(HttpRequestMessage request)
Expand All @@ -41,7 +41,7 @@ public async Task<bool> AuthenticateRequest(HttpRequestMessage request)
{
throw new AuthenticationException("The request has invalid MAuth authentication headers.", ex);
}
catch (HttpRequestException ex)
catch (RetriedRequestException ex)
{
throw new AuthenticationException(
"Could not query the application information for the application from the MAuth server.", ex);
Expand All @@ -54,46 +54,20 @@ public async Task<bool> AuthenticateRequest(HttpRequestMessage request)
catch (Exception ex)
{
throw new AuthenticationException(
"An unexpected error occured during authentication. Please see the inner exception for details",
"An unexpected error occured during authentication. Please see the inner exception for details.",
ex
);
}
}

private async Task<ApplicationInfo> GetApplicationInfo(Guid applicationUuid)
{
var signingHandler = new MAuthSigningHandler(
options: new MAuthSigningOptions()
{
ApplicationUuid = options.ApplicationUuid,
PrivateKey = options.PrivateKey
},
innerHandler: options.MAuthServerHandler ??
#if NETSTANDARD1_4
new HttpClientHandler()
#else
new WebRequestHandler()
{
CachePolicy = new RequestCachePolicy(RequestCacheLevel.Default)
}
#endif
);

using (var client = new HttpClient(signingHandler))
{
client.Timeout = TimeSpan.FromSeconds(options.AuthenticateRequestTimeoutSeconds);

var response = await client.SendAsync(new HttpRequestMessage(
HttpMethod.Get,
new Uri(
options.MAuthServiceUrl,
$"{Constants.MAuthTokenRequestPath}{applicationUuid.ToHyphenString()}.json")
)).ConfigureAwait(continueOnCapturedContext: false);

response.EnsureSuccessStatusCode();
private async Task<ApplicationInfo> GetApplicationInfo(Guid applicationUuid) =>
await (await retrier.GetSuccessfulResponse(applicationUuid, CreateRequest,
remainingAttempts: (int)options.MAuthServiceRetryPolicy + 1))
.Content
.FromResponse();

return await response.Content.FromResponse();
}
}
private HttpRequestMessage CreateRequest(Guid applicationUuid) =>
new HttpRequestMessage(HttpMethod.Get, new Uri(options.MAuthServiceUrl,
$"{Constants.MAuthTokenRequestPath}{applicationUuid.ToHyphenString()}.json"));
}
}
2 changes: 1 addition & 1 deletion src/Medidata.MAuth.Core/MAuthCoreExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ public static PayloadAuthenticationInfo GetAuthenticationInfo(this HttpRequestMe
var authHeader = request.Headers.GetFirstValueOrDefault<string>(Constants.MAuthHeaderKey);

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

var signedTime = request.Headers.GetFirstValueOrDefault<long>(Constants.MAuthTimeHeaderKey);

Expand Down
68 changes: 68 additions & 0 deletions src/Medidata.MAuth.Core/MAuthRequestRetrier.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using System;
using System.Collections.Generic;
using System.Linq;
#if !NETSTANDARD1_4
using System.Net.Cache;
#endif
using System.Net.Http;
using System.Threading.Tasks;

namespace Medidata.MAuth.Core
{
internal class MAuthRequestRetrier
{
private readonly HttpClient client;
private RetriedRequestException exception;

public MAuthRequestRetrier(MAuthOptionsBase options)
{
var signingHandler = new MAuthSigningHandler(options: new MAuthSigningOptions()
{
ApplicationUuid = options.ApplicationUuid,
PrivateKey = options.PrivateKey
},
innerHandler: options.MAuthServerHandler ??
#if NETSTANDARD1_4
new HttpClientHandler()
#else
new WebRequestHandler()
{
CachePolicy = new RequestCachePolicy(RequestCacheLevel.Default)
}
#endif
);

client = new HttpClient(signingHandler);
client.Timeout = TimeSpan.FromSeconds(options.AuthenticateRequestTimeoutSeconds);
}

public async Task<HttpResponseMessage> GetSuccessfulResponse(Guid applicationUuid,
Func<Guid, HttpRequestMessage> requestFactory, int remainingAttempts)
{
var request = requestFactory?.Invoke(applicationUuid);

if (request == null)
throw new ArgumentNullException(
nameof(requestFactory),
"No request function provided or the provided request function resulted null request."
);

exception = exception ?? new RetriedRequestException(
$"Could not get a successful response from the MAuth Service after {remainingAttempts} attempts. " +
"Please see the responses for each attempt in the exception's Responses field.")
{
Request = request
};

if (remainingAttempts == 0)
throw exception;

var result = await client.SendAsync(request).ConfigureAwait(continueOnCapturedContext: false);

exception.Responses.Add(result);

return result.IsSuccessStatusCode ?
result : await GetSuccessfulResponse(applicationUuid, requestFactory, remainingAttempts - 1);
}
}
}
27 changes: 27 additions & 0 deletions src/Medidata.MAuth.Core/MAuthServiceRetryPolicy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
namespace Medidata.MAuth.Core
{
/// <summary>
/// Determines the retrying policy with the MAuth server communication.
/// </summary>
public enum MAuthServiceRetryPolicy
{
/// <summary>
/// No attempt to retry the service request. The authentication will fail upon the first attempt if it is
/// not successful.
/// </summary>
NoRetry = 0,
/// <summary>
/// 1 more attempt to send a request to the MAuth service if the first attempt fails.
/// </summary>
RetryOnce = 1,
/// <summary>
/// 2 more attempts to send a request to the MAuth service if the first attempt fails.
/// </summary>
RetryTwice = 2,
/// <summary>
/// 9 more attempts to send a request to the MAuth service if the first attempt fails. This setting is not
/// recommended in production use as it can put more load the the MAuth service.
/// </summary>
Agressive = 9
}
}
8 changes: 7 additions & 1 deletion src/Medidata.MAuth.Core/MAuthSigningHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ public class MAuthSigningHandler: DelegatingHandler
/// <see cref="MAuthSigningOptions"/>.
/// </summary>
/// <param name="options">The options for this message handler.</param>
public MAuthSigningHandler(MAuthSigningOptions options): this(options, new HttpClientHandler()) { }
public MAuthSigningHandler(MAuthSigningOptions options)
{
this.options = options;
}

/// <summary>
/// Initializes a new instance of the <see cref="MAuthSigningHandler"/> class with the provided
Expand All @@ -47,6 +50,9 @@ public MAuthSigningHandler(MAuthSigningOptions options, HttpMessageHandler inner
protected async override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
if (InnerHandler == null)
InnerHandler = new HttpClientHandler();

return await base
.SendAsync(await request.Sign(options), cancellationToken)
.ConfigureAwait(continueOnCapturedContext: false);
Expand Down
11 changes: 9 additions & 2 deletions src/Medidata.MAuth.Core/Options/MAuthOptionsBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,16 @@ public abstract class MAuthOptionsBase
/// <summary>
/// Determines the timeout in seconds for the MAuth authentication request - the MAuth component will try to
/// reach the MAuth server for this duration before throws an exception. If not specified, the default
/// value will be 10 seconds.
/// value will be 3 seconds.
/// </summary>
public int AuthenticateRequestTimeoutSeconds { get; set; } = 10;
public int AuthenticateRequestTimeoutSeconds { get; set; } = 3;

/// <summary>
/// Determines the number of request retry attempts when communicating with the MAuth authentication service,
/// and the result is not success. If not specified, the default value will be
/// <see cref="MAuthServiceRetryPolicy.RetryOnce"/> (1 more attempt additionally for the original request).
/// </summary>
public MAuthServiceRetryPolicy MAuthServiceRetryPolicy { get; set; } = MAuthServiceRetryPolicy.RetryOnce;

/// <summary>
/// Determines the message handler for the requests to the MAuth server.
Expand Down
3 changes: 1 addition & 2 deletions src/Medidata.MAuth.Core/project.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "2.1.2-*",
"version": "2.2.0-*",
"title": "Medidata.MAuth.Core",
"authors": [ "Medidata Solutions, Inc." ],
"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.",
Expand All @@ -10,7 +10,6 @@
"projectUrl": "https://github.com/mdsol/mauth-client-dotnet",
"licenseUrl": "https://github.com/mdsol/mauth-client-dotnet/blob/master/LICENSE.md",
"summary": "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.",
"releaseNotes": "This version introduces the MAuthSigningOptions options object instead of the MAuthOptions for the MAuthSigningHandler. With this change the handler does not need the MAuth server url for its configuration anymore.",
"tags": [ "medidata", "mauth", "hmac", "authentication", "core", "httpclient", "messagehandler" ]
},

Expand Down
3 changes: 1 addition & 2 deletions src/Medidata.MAuth.Owin/project.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "2.1.1-*",
"version": "2.2.0-*",
"title": "Medidata.MAuth.Owin",
"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.",
"copyright": "Copyright © Medidata Solutions, Inc. 2017",
Expand All @@ -11,7 +11,6 @@
"projectUrl": "https://github.com/mdsol/mauth-client-dotnet",
"licenseUrl": "https://github.com/mdsol/mauth-client-dotnet/blob/master/LICENSE.md",
"summary": "An OWIN middleware for the core Medidata.MAuth module.",
"releaseNotes": "This version makes possible to read the OWIN request body in the subsequent middlewares after the authentication, even if the body stream is not seekable initially.",
"tags": [ "medidata", "mauth", "hmac", "authentication", "core", "owin", "middleware", "webapi" ]
},

Expand Down
9 changes: 8 additions & 1 deletion src/Medidata.MAuth.WebApi/MAuthAuthenticatingHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ public class MAuthAuthenticatingHandler : DelegatingHandler
/// <see cref="MAuthWebApiOptions"/>.
/// </summary>
/// <param name="options">The options for this message handler.</param>
public MAuthAuthenticatingHandler(MAuthWebApiOptions options) : this(options, new HttpClientHandler()) { }
public MAuthAuthenticatingHandler(MAuthWebApiOptions options)
{
this.options = options;
authenticator = new MAuthAuthenticator(options);
}

/// <summary>
/// Initializes a new instance of the <see cref="MAuthAuthenticatingHandler"/> class with the provided
Expand Down Expand Up @@ -52,6 +56,9 @@ public MAuthAuthenticatingHandler(MAuthWebApiOptions options, HttpMessageHandler
protected async override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
if (InnerHandler == null)
InnerHandler = new HttpClientHandler();

if (!await request.TryAuthenticate(authenticator, options.HideExceptionsAndReturnForbidden))
return new HttpResponseMessage(HttpStatusCode.Forbidden) { RequestMessage = request };

Expand Down
Loading

0 comments on commit 4367b95

Please sign in to comment.