Skip to content

Commit

Permalink
Hash algorithm in header (#17)
Browse files Browse the repository at this point in the history
* add hmac hashing and request body hashing algorithm in header

* remove AllowMD5AndSHA256RequestBodyHash, add unit tess for all combinations of HmacHashingMethod and RequestBodyHashingMethod

* add unit tests for old headers without hashing methods
  • Loading branch information
meinsiedler authored Mar 27, 2023
1 parent 3a5ffd7 commit f1436c9
Show file tree
Hide file tree
Showing 12 changed files with 312 additions and 194 deletions.
159 changes: 89 additions & 70 deletions softaware.Authentication.Hmac.AspNetCore.Test/MiddlewareTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,66 @@ namespace softaware.Authentication.Hmac.AspNetCore.Test
{
public class MiddlewareTest
{
[Fact]
public Task Request_Authorized()
public static IEnumerable<HmacHashingMethod> GetHmacHashingMethods() => Enum.GetValues<HmacHashingMethod>();
public static IEnumerable<RequestBodyHashingMethod> GetRequestBodyHashingMethods() => Enum.GetValues<RequestBodyHashingMethod>();

[Theory]
[CombinatorialData]
public Task Request_Authorized(
[CombinatorialMemberData(nameof(GetHmacHashingMethods))] HmacHashingMethod hmacHashingMethod,
[CombinatorialMemberData(nameof(GetRequestBodyHashingMethods))] RequestBodyHashingMethod requestBodyHashingMethod)
{
return this.TestRequestAsync(
new Dictionary<string, string>() { { "appId", "MNpx/353+rW+pqv8UbRTAtO1yoabl8/RFDAv/615u5w=" } },
"appId",
"MNpx/353+rW+pqv8UbRTAtO1yoabl8/RFDAv/615u5w=",
hmacHashingMethod,
requestBodyHashingMethod,
HttpStatusCode.OK);
}

[Fact]
public Task Request_WithoutHashingMethodsInHeader_MD5AndHMACSHA256Used_Authorized()
{
return this.TestRequestAsync(
new Dictionary<string, string>() { { "appId", "MNpx/353+rW+pqv8UbRTAtO1yoabl8/RFDAv/615u5w=" } },
"appId",
"MNpx/353+rW+pqv8UbRTAtO1yoabl8/RFDAv/615u5w=",
hmacHashingMethod: HmacHashingMethod.HMACSHA256,
requestBodyHashingMethod: RequestBodyHashingMethod.MD5,
HttpStatusCode.OK,
removeHashingAlgorithmFromHeader: true);
}

/// <summary>
/// If no hashing algorithm is sent in header, the default values HMAC256 and MD5 are assumed.
/// (Default values from previous library version.)
/// If different method is specified in <see cref="ApiKeyDelegatingHandler"/>, the server assumes invalid signature.
/// </summary>
/// <remarks>
/// This test ensures that older versions of the <see cref="ApiKeyDelegatingHandler"/>, which doesn't send hashing algorithm header,
/// still works with the default values.
/// </remarks>
[Theory]
[InlineData(HmacHashingMethod.HMACSHA256, RequestBodyHashingMethod.MD5, HttpStatusCode.OK)]
[InlineData(HmacHashingMethod.HMACSHA512, RequestBodyHashingMethod.MD5, HttpStatusCode.Unauthorized)]
[InlineData(HmacHashingMethod.HMACSHA256, RequestBodyHashingMethod.SHA256, HttpStatusCode.Unauthorized)]
[InlineData(HmacHashingMethod.HMACSHA512, RequestBodyHashingMethod.SHA256, HttpStatusCode.Unauthorized)]
public Task Request_WithoutHashingMethodsInHeader(
HmacHashingMethod hmacHashingMethod,
RequestBodyHashingMethod requestBodyHashingMethod,
HttpStatusCode httpStatusCode)
{
return this.TestRequestAsync(
new Dictionary<string, string>() { { "appId", "MNpx/353+rW+pqv8UbRTAtO1yoabl8/RFDAv/615u5w=" } },
"appId",
"MNpx/353+rW+pqv8UbRTAtO1yoabl8/RFDAv/615u5w=",
hmacHashingMethod,
requestBodyHashingMethod,
httpStatusCode,
removeHashingAlgorithmFromHeader: true);
}

[Fact]
public async Task Request_WithDeprecatedHmacAuthorizedAppsOption_Authorized()
{
Expand Down Expand Up @@ -79,53 +129,6 @@ public async Task Request_Unauthorized_WithTrustProxy()
}
}

[Theory]
[InlineData(RequestBodyHashingMethod.MD5)]
[InlineData(RequestBodyHashingMethod.SHA256)]
public async Task Request_Authorized_WithMD5ClientAndMD5AllowedOnServer(RequestBodyHashingMethod requestBodyHashingMethod)
{
using (var client = this.GetHttpClientWithAllowMD5AndSHA256RequestBodyHashOption(
new Dictionary<string, string>() { { "appId", "MNpx/353+rW+pqv8UbRTAtO1yoabl8/RFDAv/615u5w=" } },
"appId",
"MNpx/353+rW+pqv8UbRTAtO1yoabl8/RFDAv/615u5w=",
allowMD5AndSHA256RequestBodyHash: true,
requestBodyHashingMethod: requestBodyHashingMethod))
{
var response = await client.PostAsync("api/test", new StringContent("test"));
Assert.True(response.StatusCode == HttpStatusCode.OK);
}
}

[Fact]
public async Task Request_Unauthorized_MD5ClientAndMD5NotAllowedOnServer()
{
using (var client = this.GetHttpClientWithAllowMD5AndSHA256RequestBodyHashOption(
new Dictionary<string, string>() { { "appId", "MNpx/353+rW+pqv8UbRTAtO1yoabl8/RFDAv/615u5w=" } },
"appId",
"MNpx/353+rW+pqv8UbRTAtO1yoabl8/RFDAv/615u5w=",
allowMD5AndSHA256RequestBodyHash: false,
requestBodyHashingMethod: RequestBodyHashingMethod.MD5))
{
var response = await client.PostAsync("api/test", new StringContent("test"));
Assert.True(response.StatusCode == HttpStatusCode.Unauthorized);
}
}

[Fact]
public async Task Request_Authorized_SHA256ClientAndMD5NotAllowedOnServer()
{
using (var client = this.GetHttpClientWithAllowMD5AndSHA256RequestBodyHashOption(
new Dictionary<string, string>() { { "appId", "MNpx/353+rW+pqv8UbRTAtO1yoabl8/RFDAv/615u5w=" } },
"appId",
"MNpx/353+rW+pqv8UbRTAtO1yoabl8/RFDAv/615u5w=",
allowMD5AndSHA256RequestBodyHash: false,
requestBodyHashingMethod: RequestBodyHashingMethod.SHA256))
{
var response = await client.PostAsync("api/test", new StringContent("test"));
Assert.True(response.StatusCode == HttpStatusCode.OK);
}
}

[Theory]
[InlineData("appId", "YXJld3JzZHJkc2FhcndlZQ==")]
[InlineData("wrongAppId", "MNpx/353+rW+pqv8UbRTAtO1yoabl8/RFDAv/615u5w=")]
Expand All @@ -135,6 +138,8 @@ public Task Request_Unauthorized(string appId, string apiKey)
new Dictionary<string, string>() { { "appId", "MNpx/353+rW+pqv8UbRTAtO1yoabl8/RFDAv/615u5w=" } },
appId,
apiKey,
HmacHashingMethod.HMACSHA256,
RequestBodyHashingMethod.MD5,
HttpStatusCode.Unauthorized);
}

Expand All @@ -147,6 +152,8 @@ public Task Request_ApiKeyBadFormat_ThrowsException(string appId, string apiKey)
new Dictionary<string, string>() { { "appId", "MNpx/353+rW+pqv8UbRTAtO1yoabl8/RFDAv/615u5w=" } },
appId,
apiKey,
HmacHashingMethod.HMACSHA256,
RequestBodyHashingMethod.MD5,
HttpStatusCode.Unauthorized));
}

Expand All @@ -159,6 +166,8 @@ public async Task Request_Authorized_UsernameAppIdSet()
new Dictionary<string, string>() { { appId, "MNpx/353+rW+pqv8UbRTAtO1yoabl8/RFDAv/615u5w=" } },
appId,
"MNpx/353+rW+pqv8UbRTAtO1yoabl8/RFDAv/615u5w=",
HmacHashingMethod.HMACSHA256,
RequestBodyHashingMethod.MD5,
HttpStatusCode.OK,
"api/test/name");

Expand All @@ -176,6 +185,8 @@ public async Task Request_Authorized_Claims()
new Dictionary<string, string>() { { appId, "MNpx/353+rW+pqv8UbRTAtO1yoabl8/RFDAv/615u5w=" } },
appId,
"MNpx/353+rW+pqv8UbRTAtO1yoabl8/RFDAv/615u5w=",
HmacHashingMethod.HMACSHA256,
RequestBodyHashingMethod.MD5,
HttpStatusCode.OK,
"api/test/claims");

Expand All @@ -191,28 +202,51 @@ private async Task<HttpResponseMessage> TestRequestAsync(
IDictionary<string, string> authenticatedApps,
string appId,
string apiKey,
HmacHashingMethod hmacHashingMethod,
RequestBodyHashingMethod requestBodyHashingMethod,
HttpStatusCode expectedStatusCode,
string endpoint = "api/test")
string endpoint = "api/test",
bool removeHashingAlgorithmFromHeader = false)
{
using (var client = this.GetHttpClient(
authenticatedApps,
appId,
apiKey))
apiKey,
hmacHashingMethod,
requestBodyHashingMethod,
removeHashingAlgorithmFromHeader))
{
var response = await client.GetAsync(endpoint);
Assert.True(response.StatusCode == expectedStatusCode);
var response = await client.PostAsync(endpoint, new StringContent("test-content"));
Assert.Equal(expectedStatusCode, response.StatusCode);

return response;
}
}

private HttpClient GetHttpClient(IDictionary<string, string> hmacAuthenticatedApps, string appId, string apiKey)
private HttpClient GetHttpClient(
IDictionary<string, string> hmacAuthenticatedApps,
string appId,
string apiKey,
HmacHashingMethod hmacHashingMethod,
RequestBodyHashingMethod requestBodyHashingMethod,
bool removeHashingAlgorithmFromHeader)
{
var factory = new TestWebApplicationFactory(o =>
{
o.AuthorizationProvider = new MemoryHmacAuthenticationProvider(hmacAuthenticatedApps);
});
return factory.CreateDefaultClient(new ApiKeyDelegatingHandler(appId, apiKey));

var handlers = new List<DelegatingHandler>
{
new ApiKeyDelegatingHandler(appId, apiKey, hmacHashingMethod, requestBodyHashingMethod)
};

if (removeHashingAlgorithmFromHeader)
{
handlers.Add(new RemoveHashingMethodDelegatingHandler());
}

return factory.CreateDefaultClient(handlers.ToArray());
}

private HttpClient GetHttpClientWithHmacAutenticatedAppsOption(IDictionary<string, string> hmacAuthenticatedApps, string appId, string apiKey)
Expand All @@ -239,20 +273,5 @@ private HttpClient GetHttpClientWithTrustProxyOption(IDictionary<string, string>
});
return factory.CreateDefaultClient(new ApiKeyDelegatingHandler(appId, apiKey));
}

private HttpClient GetHttpClientWithAllowMD5AndSHA256RequestBodyHashOption(
IDictionary<string, string> hmacAuthenticatedApps,
string appId,
string apiKey,
bool allowMD5AndSHA256RequestBodyHash,
RequestBodyHashingMethod requestBodyHashingMethod)
{
var factory = new TestWebApplicationFactory(o =>
{
o.AuthorizationProvider = new MemoryHmacAuthenticationProvider(hmacAuthenticatedApps);
o.AllowMD5AndSHA256RequestBodyHash = allowMD5AndSHA256RequestBodyHash;
});
return factory.CreateDefaultClient(new ApiKeyDelegatingHandler(appId, apiKey, requestBodyHashingMethod));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

namespace softaware.Authentication.Hmac.AspNetCore.Test
{
internal class RemoveHashingMethodDelegatingHandler : DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var hmacAuthHeaderValue = request.Headers.Authorization?.Parameter;
if (hmacAuthHeaderValue != null)
{
var values = hmacAuthHeaderValue.Split(":").ToList();

if (values.Count == 6) // Hmac header has HmacHashingAlgorithm and RequestBodyHashingAlgorithm paramemters set
{
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue(
request.Headers.Authorization.Scheme,
string.Join(":", values.Skip(2))); // remove first two parameters (= HmacHashingAlgorithm and RequestBodyHashingAlgorithm)
}
}

return base.SendAsync(request, cancellationToken);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="Xunit.Combinatorial" Version="1.5.25" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,6 @@ public class HmacAuthenticationSchemeOptions : AuthenticationSchemeOptions
/// </summary>
public bool TrustProxy { get; set; }

/// <summary>
/// If <see langword="true"/>, the request body hash will be validated with MD5 hash and SHA265 hash.
/// Note that this setting is only relevant when the http request has a body.
/// (Default: <see langword="true"/>)
/// </summary>
/// <remarks>
/// This setting helps upgrading from MD5 to SHA256 hash without breaking changes.
/// </remarks>
[Obsolete("Will be removed in the next major version as only SHA256 request body hashing will be supported in future.")]
public bool AllowMD5AndSHA256RequestBodyHash { get; set; } = true;

private IDictionary<string, string> hmacAuthenticatedApps = new Dictionary<string, string>();

[Obsolete("Please use the MemoryHmacAuthenticationProvider for configuring the HMAC apps in-memory. This property will be removed in future versions of this package.", error: false)]
Expand Down
Loading

0 comments on commit f1436c9

Please sign in to comment.