Skip to content

Commit

Permalink
Adding case in-/sensitivity option to header and query keys
Browse files Browse the repository at this point in the history
  • Loading branch information
dnmh-psc committed Jul 13, 2023
1 parent 140b83a commit 5705a0c
Show file tree
Hide file tree
Showing 7 changed files with 324 additions and 5 deletions.
8 changes: 4 additions & 4 deletions Source/AuthenticationHandler/ApiKeyAuthenticationOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ public class ApiKeyAuthenticationOptions : AuthenticationSchemeOptions
/// <summary>
/// The default value for <see cref="QueryKeys"/> if none are added.
/// </summary>
public const string DefaultQueryKey = "apikey";
public static readonly Key DefaultQueryKey = new("apikey", true);

/// <summary>
/// The default value for <see cref="HeaderKeys"/> if none are added.
/// </summary>
public const string DefaultHeaderKey = "X-API-KEY";
public static readonly Key DefaultHeaderKey = new("X-API-KEY", true);

/// <inheritdoc/>
public new ApiKeyAuthenticationEvents? Events
Expand Down Expand Up @@ -49,7 +49,7 @@ public class ApiKeyAuthenticationOptions : AuthenticationSchemeOptions
/// Example of query:
/// <code>&lt;QueryKey&gt;=abcdef12345</code>
/// </example>
public ISet<string> QueryKeys { get; set; } = new HashSet<string>();
public ISet<Key> QueryKeys { get; set; } = new HashSet<Key>();

/// <summary>
/// Get or set the allowed keys used for the api key, when provided as a request header parameter.
Expand All @@ -62,7 +62,7 @@ public class ApiKeyAuthenticationOptions : AuthenticationSchemeOptions
/// Example of header, if the <see cref="HeaderKeys"/> contains <c>X-API-KEY</c>:
/// <code>X-API-KEY: abcdef12345</code>
/// </example>
public ISet<string> HeaderKeys { get; set; } = new HashSet<string>();
public ISet<Key> HeaderKeys { get; set; } = new HashSet<Key>();

/// <summary>
/// If <c>true</c>, then the standard <c>Authorization</c> header key is used in combination with <see cref="AuthorizationSchemeInHeader"/>.
Expand Down
35 changes: 35 additions & 0 deletions Source/AuthenticationHandler/Internal/KeyExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
namespace Dnmh.Security.ApiKeyAuthentication.AuthenticationHandler.Internal;

/// <summary>
/// Extension methods for <see cref="Key"/>
/// </summary>
internal static class KeyExtensions
{
/// <summary>
/// Intersects an <see cref="IEnumerable{T}"/> with type <see cref="Key"/> and an <see cref="IEnumerable{T}"/> with type <see cref="string"/> using each key's <see cref="Key.StringComparer"/>
/// </summary>
public static IEnumerable<Key> Intersect(this IEnumerable<Key> first, IEnumerable<string> second)
{
ArgumentNullException.ThrowIfNull(first, nameof(first));
ArgumentNullException.ThrowIfNull(second, nameof(second));

return first.Where(x => second.Contains(x.Name, x.StringComparer));
}

/// <summary>
/// Determines if an <see cref="IEnumerable{T}"/> with type <see cref="Key"/> contains the given <paramref name="value"/> using each key's <see cref="Key.StringComparer"/>
/// </summary>
public static bool Contains(this IEnumerable<Key> keys, string value)
{
ArgumentNullException.ThrowIfNull(keys, nameof(keys));

foreach (var key in keys)
{
if (key.StringComparer.Equals(key.Name, value))
{
return true;
}
}
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public PostConfigureSwaggerAuthorization(string authenticationScheme, IOptionsMo
public void PostConfigure(string? name, SwaggerGenOptions options)
{
var parameterLocation = _authenticationOptions.AllowApiKeyInQuery ? ParameterLocation.Query : ParameterLocation.Header;
var keyName = parameterLocation == ParameterLocation.Query ? _authenticationOptions.QueryKeys.First() : _authenticationOptions.HeaderKeys.First();
var keyName = parameterLocation == ParameterLocation.Query ? _authenticationOptions.QueryKeys.First().Name : _authenticationOptions.HeaderKeys.First().Name;
var scheme = _authenticationOptions.UseSchemeNameInAuthorizationHeader ? _swaggerSchemeOptions.AuthenticationScheme : _authenticationOptions.AuthorizationSchemeInHeader;
// Setup the security definition
options.AddSecurityDefinition(keyName, new OpenApiSecurityScheme
Expand Down
91 changes: 91 additions & 0 deletions Source/AuthenticationHandler/Key.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
namespace Dnmh.Security.ApiKeyAuthentication.AuthenticationHandler;

/// <summary>
/// A key with a string value and a case in-/sensitive boolean value
/// </summary>
public class Key : IEquatable<Key>
{
/// <summary>
/// The name of the key
/// </summary>
public string Name { get; }

/// <summary>
/// Indicates if the key is case sensitive
/// </summary>
public bool IsCaseSensitive { get; }

/// <summary>
/// <see cref="System.StringComparer"/> based on <see cref="IsCaseSensitive"/>
/// </summary>
public StringComparer StringComparer { get; }

/// <summary>
/// Creates a new instance
/// </summary>
/// <param name="name">The name of the key</param>
/// <param name="isCaseSensitive">Indicates wether or not the key is case sensitive or not</param>
public Key(string name, bool isCaseSensitive)
{
ArgumentNullException.ThrowIfNull(name, nameof(name));

Name = name;
IsCaseSensitive = isCaseSensitive;
StringComparer = StringComparer.FromComparison(IsCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase);
}

/// <inheritdoc/>
public override bool Equals(object? obj) => Equals(obj as Key);

/// <inheritdoc/>
public bool Equals(Key? other)
{
if (other is null)
{
return false;
}

// Optimization for a common success case.
if (ReferenceEquals(this, other))
{
return true;
}

// If run-time types are not exactly the same, return false.
if (GetType() != other.GetType())
{
return false;
}

// Return true if the fields match.
// If either of the StringComparers return true, then they are equal (one might be case sensitive, and the other not - the non-case sensitive then overrules)
return StringComparer.Equals(Name, other.Name) || other.StringComparer.Equals(Name, other.Name);
}

/// <inheritdoc/>
public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Name); // using case-insensitive HashCode here, in order to trigger an equality test (using .Equals) on hash collisions.

/// <summary>
/// Tests equality
/// </summary>
public static bool operator ==(Key lhs, Key rhs)
{
if (lhs is null)
{
if (rhs is null)
{
return true;
}

// Only the left side is null.
return false;
}
// Equals handles case of null on right side.
return lhs.Equals(rhs);
}

/// <summary>
/// Tests inequality
/// </summary>
public static bool operator !=(Key lhs, Key rhs) => !(lhs == rhs);
}
13 changes: 13 additions & 0 deletions Source/AuthenticationHandler/KeyHashSetExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace Dnmh.Security.ApiKeyAuthentication.AuthenticationHandler;

/// <summary>
/// Extension methods for <see cref="ISet{Task}"/> with type <see cref="Key"/>
/// </summary>
public static class KeyHashSetExtensions
{
/// <summary>
/// Adds a new <see cref="Key"/> to the set
/// </summary>
public static bool Add(this ISet<Key> set, string value, bool isCaseSensitive = true) =>
set.Add(new Key(value, isCaseSensitive));
}
65 changes: 65 additions & 0 deletions Tests/AuthenticationHandler/ApiKeyAuthenticationHandlerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,28 @@ public async Task TestOptionsWithMultipleCustomHeaderKeysSuccessfully()
result.Succeeded.Should().BeTrue();
}

[Fact]
public async Task TestOptionsWithCaseInsensitiveCustomHeaderKeysSuccessfully()
{
// Arrange
var optionsMock = MockHelpers.CreateMockOptionsMonitor<ApiKeyAuthenticationOptions>(options =>
{
options.AllowApiKeyInQuery = false;
options.AllowApiKeyInRequestHeader = true;
options.HeaderKeys.Add("X-ApiKey", false);
});
var handler = MockHelpers.CreateApiKeyAuthenticationHandler(optionsMock.Object);
var mockHttpContext = MockHelpers.CreateMockHttpContextWithRequestHeaders(new Dictionary<string, StringValues> { { "x-apikey", "key" } });
await handler.InitializeWithSchemeNameAsync(mockHttpContext.Object);

// Act
var result = await handler.AuthenticateAsync();

// Assert
result.Should().NotBeNull();
result.Succeeded.Should().BeTrue();
}

[Fact]
public async Task TestOptionsWithMultipleCustomHeaderKeysButNoneMatchShouldFail()
{
Expand Down Expand Up @@ -276,4 +298,47 @@ public async Task TestOptionsWithMultipleCustomQueryKeysButNoneMatchShouldFail()
result.Should().NotBeNull();
result.Succeeded.Should().BeFalse();
}

[Fact]
public async Task TestOptionsWithCaseInsensitiveCustomQueryKeySuccessfully()
{
// Arrange
var optionsMock = MockHelpers.CreateMockOptionsMonitor<ApiKeyAuthenticationOptions>(options =>
{
options.QueryKeys.Add("mykey", false);
});
var handler = MockHelpers.CreateApiKeyAuthenticationHandler(optionsMock.Object);
var mockHttpContext = MockHelpers.CreateMockHttpContextWithRequestQueryParams(new Dictionary<string, StringValues> { { "MYKEY", "key" } });
await handler.InitializeWithSchemeNameAsync(mockHttpContext.Object);

// Act
var result = await handler.AuthenticateAsync();

// Assert
result.Should().NotBeNull();
result.Succeeded.Should().BeTrue();
}

[Fact]
public async Task TestOptionsWithMultipleCaseInsensitiveCustomQueryKeySuccessfully()
{
// Arrange
var optionsMock = MockHelpers.CreateMockOptionsMonitor<ApiKeyAuthenticationOptions>(options =>
{
options.QueryKeys.Add("mykey", false);
options.QueryKeys.Add("otherkey", true);
});
var handler = MockHelpers.CreateApiKeyAuthenticationHandler(optionsMock.Object);
var mockHttpContext = MockHelpers.CreateMockHttpContextWithRequestQueryParams(new Dictionary<string, StringValues> { { "MYKEY", "key" }, { "OTHERKEY", "key" } });
await handler.InitializeWithSchemeNameAsync(mockHttpContext.Object);

// Act
var result = await handler.AuthenticateAsync();

// Assert
result.Should().NotBeNull();
result.Succeeded.Should().BeTrue();
}


}
115 changes: 115 additions & 0 deletions Tests/AuthenticationHandler/KeyTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
using Dnmh.Security.ApiKeyAuthentication.AuthenticationHandler;
using FluentAssertions;

namespace Dnmh.Security.ApiKeyAuthentication.Tests.AuthenticationHandler;

public class KeyTest
{
[Fact]
public void KeyEqualityTest()
{
// Arrange
var key1 = new Key("key", true);
var key2 = new Key("key", true);

// Act & Assert
key1.Should().Be(key2);
}

[Fact]
public void KeyInequalityTest()
{
// Arrange
var key1 = new Key("key", true);
var key2 = new Key("KEY", true);

// Act & Assert
key1.Should().NotBe(key2);
}

[Fact]
public void KeyCaseInsensitiveEqualityTest()
{
// Arrange
var key1 = new Key("key", true);
var key2 = new Key("KEY", false);

// Act & Assert
key1.Should().Be(key2);
}

[Fact]
public void KeyCaseInsensitiveEqualityReverseTest()
{
// Arrange
var key1 = new Key("key", false);
var key2 = new Key("KEY", true);

// Act & Assert
key1.Should().Be(key2);
}

[Fact]
public void KeyHashSetAddSameKeyTest()
{
// Arrange
var key1 = new Key("key", true);
var key2 = new Key("key", true);

// Act
var set = new HashSet<Key>() { key1 };
var result = set.Add(key2);

// Assert
result.Should().BeFalse();
set.Should().ContainSingle();
}

[Fact]
public void KeyHashSetAddDifferentKeyTest()
{
// Arrange
var key1 = new Key("key", true);
var key2 = new Key("KEY", true);

// Act
var set = new HashSet<Key>() { key1 };
var result = set.Add(key2);

// Assert
result.Should().BeTrue();
set.Should().HaveCount(2);
}

[Fact]
public void KeyHashSetAddSameCaseInsensitiveKeyTest()
{
// Arrange
var key1 = new Key("key", true);
var key2 = new Key("KEY", false);

// Act
var set = new HashSet<Key>() { key1 };
var result = set.Add(key2);

// Assert
result.Should().BeFalse();
set.Should().ContainSingle();
}

[Fact]
public void KeyHashSetAddSameCaseInsensitiveKeyReverseTest()
{
// Arrange
var key1 = new Key("key", false);
var key2 = new Key("KEY", true);

// Act
var set = new HashSet<Key>() { key1 };
var result = set.Add(key2);

// Assert
result.Should().BeFalse();
set.Should().ContainSingle();
}
}

0 comments on commit 5705a0c

Please sign in to comment.