Adding security
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace LCU.Library.Internal.ApiSecurity;
|
||||
|
||||
public class ApiSecurityBuilder : IApiSecurityBuilder
|
||||
{
|
||||
public ApiSecurityBuilder(IServiceCollection services)
|
||||
{
|
||||
Services = services ?? throw new ArgumentNullException(nameof(services));
|
||||
}
|
||||
|
||||
public IServiceCollection Services { get; }
|
||||
}
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
using LCU.Library.Internal.ApiSecurity.Constants;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using LCU.Library.Internal.ApiSecurity.Enums;
|
||||
using LCU.Library.Internal.ApiSecurity.Models;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LCU.Library.Internal.ApiSecurity.Attributes;
|
||||
|
||||
[AttributeUsage(validOn: AttributeTargets.Method)]
|
||||
public class ApiKeyAttribute : Attribute, IAsyncActionFilter
|
||||
{
|
||||
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
|
||||
{
|
||||
var apiSecurityConfiguration = context.HttpContext.RequestServices.GetRequiredService<ApiSecurityConfiguration>();
|
||||
var logger = context.HttpContext.RequestServices.GetRequiredService<ILoggerFactory>().CreateLogger(GetType().Name);
|
||||
|
||||
if (apiSecurityConfiguration.ExternalApiKeySet.Any())
|
||||
{
|
||||
if (!context.HttpContext.Request.Headers.TryGetValue(HeaderKeys.ApiKey, out var extractedApiKey))
|
||||
{
|
||||
switch (apiSecurityConfiguration.ApiKeyAuthenticationFailureBehavior)
|
||||
{
|
||||
case ApiKeyAuthenticationFailureBehavior.Warn:
|
||||
logger.LogWarning(AuthMessages.NotProvidedMessage(HeaderKeys.ApiKey));
|
||||
break;
|
||||
case ApiKeyAuthenticationFailureBehavior.Error:
|
||||
logger.LogError(AuthMessages.NotProvidedMessage(HeaderKeys.ApiKey));
|
||||
context.Result = new ContentResult()
|
||||
{
|
||||
StatusCode = 401,
|
||||
Content = AuthMessages.NotProvidedMessage(HeaderKeys.ApiKey)
|
||||
};
|
||||
return;
|
||||
default:
|
||||
throw new Exception("Invalid ApiKeyAuthenticationFailureBehavior Value");
|
||||
}
|
||||
}
|
||||
|
||||
if (!context.HttpContext.Request.Headers.TryGetValue(HeaderKeys.Client, out var extractedClient))
|
||||
{
|
||||
switch (apiSecurityConfiguration.ApiKeyAuthenticationFailureBehavior)
|
||||
{
|
||||
case ApiKeyAuthenticationFailureBehavior.Warn:
|
||||
logger.LogWarning(AuthMessages.NotProvidedMessage(HeaderKeys.Client));
|
||||
break;
|
||||
case ApiKeyAuthenticationFailureBehavior.Error:
|
||||
logger.LogError(AuthMessages.NotProvidedMessage(HeaderKeys.Client));
|
||||
context.Result = new ContentResult()
|
||||
{
|
||||
StatusCode = 401,
|
||||
Content = AuthMessages.NotProvidedMessage(HeaderKeys.Client)
|
||||
};
|
||||
return;
|
||||
default:
|
||||
throw new Exception("Invalid ApiKeyAuthenticationFailureBehavior Value");
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(extractedClient))
|
||||
{
|
||||
if (apiSecurityConfiguration.ExternalApiKeySet.TryGetValue(extractedClient, out var apiKey))
|
||||
{
|
||||
if (!apiKey.Equals(extractedApiKey))
|
||||
{
|
||||
switch (apiSecurityConfiguration.ApiKeyAuthenticationFailureBehavior)
|
||||
{
|
||||
case ApiKeyAuthenticationFailureBehavior.Warn:
|
||||
logger.LogWarning(AuthMessages.AuthFailureMessage(extractedClient));
|
||||
break;
|
||||
case ApiKeyAuthenticationFailureBehavior.Error:
|
||||
logger.LogError(AuthMessages.AuthFailureMessage(extractedClient));
|
||||
context.Result = new ContentResult()
|
||||
{
|
||||
StatusCode = 401,
|
||||
Content = AuthMessages.AuthFailureMessage(extractedClient)
|
||||
};
|
||||
return;
|
||||
default:
|
||||
throw new Exception("Invalid ApiKeyAuthenticationFailureBehavior Value");
|
||||
}
|
||||
}
|
||||
|
||||
logger.LogInformation($"Valid auth received for Client: {extractedClient}");
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (apiSecurityConfiguration.ApiKeyAuthenticationFailureBehavior)
|
||||
{
|
||||
case ApiKeyAuthenticationFailureBehavior.Warn:
|
||||
logger.LogWarning(AuthMessages.ClientNotFoundMessage());
|
||||
break;
|
||||
case ApiKeyAuthenticationFailureBehavior.Error:
|
||||
logger.LogError(AuthMessages.ClientNotFoundMessage());
|
||||
context.Result = new ContentResult()
|
||||
{
|
||||
StatusCode = 401,
|
||||
Content = AuthMessages.ClientNotFoundMessage()
|
||||
};
|
||||
return;
|
||||
default:
|
||||
throw new Exception("Invalid ApiKeyAuthenticationFailureBehavior Value");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (apiSecurityConfiguration.ApiKeyAuthenticationFailureBehavior)
|
||||
{
|
||||
case ApiKeyAuthenticationFailureBehavior.Warn:
|
||||
logger.LogWarning(AuthMessages.EmptyKeySetMessage("ExternalApiKeySet"));
|
||||
break;
|
||||
case ApiKeyAuthenticationFailureBehavior.Error:
|
||||
logger.LogError(AuthMessages.EmptyKeySetMessage("ExternalApiKeySet"));
|
||||
context.Result = new ContentResult()
|
||||
{
|
||||
StatusCode = 401,
|
||||
Content = AuthMessages.EmptyKeySetMessage("ExternalApiKeySet")
|
||||
};
|
||||
return;
|
||||
default:
|
||||
throw new Exception("Invalid ApiKeyAuthenticationFailureBehavior Value");
|
||||
}
|
||||
}
|
||||
|
||||
await next();
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
namespace LCU.Library.Internal.ApiSecurity.Constants;
|
||||
|
||||
public static class AuthMessages
|
||||
{
|
||||
private const string AuthFailureMessageTemplate = "Unauthorized client: ";
|
||||
private const string ClientNotFoundMessageTemplate = "Client not found for extracted API Key";
|
||||
private const string EmptyKeySetMessageTemplate = "Empty Key Set: ";
|
||||
private const string NotProvidedMessageTemplate = "Header Not Provided: ";
|
||||
|
||||
public static string AuthFailureMessage(string input)
|
||||
{
|
||||
return $"{AuthFailureMessageTemplate}{input}";
|
||||
}
|
||||
|
||||
public static string ClientNotFoundMessage()
|
||||
{
|
||||
return $"{ClientNotFoundMessageTemplate}";
|
||||
}
|
||||
|
||||
public static string EmptyKeySetMessage(string input)
|
||||
{
|
||||
return $"{EmptyKeySetMessageTemplate}{input}";
|
||||
}
|
||||
|
||||
public static string NotProvidedMessage(string input)
|
||||
{
|
||||
return $"{NotProvidedMessageTemplate}{input}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace LCU.Library.Internal.ApiSecurity.Constants;
|
||||
|
||||
public static class HeaderKeys
|
||||
{
|
||||
public const string ApiKey = "ApiKey";
|
||||
public const string Client = "Client";
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
namespace LCU.Library.Internal.ApiSecurity.Enums;
|
||||
|
||||
public enum ApiKeyAuthenticationFailureBehavior
|
||||
{
|
||||
Error,
|
||||
Warn
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace LCU.Library.Internal.ApiSecurity.Enums;
|
||||
|
||||
public enum ApiKeySetName
|
||||
{
|
||||
InternalKeySet,
|
||||
ExternalKeySet
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace LCU.Library.Internal.ApiSecurity;
|
||||
|
||||
public interface IApiSecurityBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// The application services.
|
||||
/// </summary>
|
||||
IServiceCollection Services { get; }
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Title>LCU.Library.Internal.ApiSecurity</Title>
|
||||
<Authors>Landmark Credit Union</Authors>
|
||||
<RepositoryType>Git</RepositoryType>
|
||||
<Company>Landmark Credit Union</Company>
|
||||
<AssemblyVersion>2.0.0</AssemblyVersion>
|
||||
<FileVersion>2.0.0</FileVersion>
|
||||
<PackageVersion>2.0.0</PackageVersion>
|
||||
<PackageId>LCU.Library.Internal.ApiSecurity</PackageId>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<Version>2.0.0</Version>
|
||||
<Description>
|
||||
Standard LCU Api Security Library
|
||||
2.0.0 - Updated to .NET 8
|
||||
</Description>
|
||||
<PackageReleaseNotes>
|
||||
Do not throw on empty key sets, account for empty sets in auth
|
||||
2.0.0 - Updated to .NET 8
|
||||
</PackageReleaseNotes>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.3.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.3.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" />
|
||||
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,13 @@
|
||||
using LCU.Library.Internal.ApiSecurity.Models;
|
||||
|
||||
namespace LCU.Library.Internal.ApiSecurity;
|
||||
|
||||
public class LcuApiSecurity
|
||||
{
|
||||
private readonly ApiSecurityConfiguration _apiSecurityConfiguration;
|
||||
|
||||
public LcuApiSecurity(ApiSecurityConfiguration apiSecurityConfiguration)
|
||||
{
|
||||
_apiSecurityConfiguration = apiSecurityConfiguration;
|
||||
}
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
using LCU.Library.Internal.ApiSecurity.Constants;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using LCU.Library.Internal.ApiSecurity.Enums;
|
||||
using LCU.Library.Internal.ApiSecurity.Models;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LCU.Library.Internal.ApiSecurity.Middleware
|
||||
{
|
||||
public class ApiKeyMiddleware
|
||||
{
|
||||
private readonly ApiSecurityConfiguration _apiSecurityConfiguration;
|
||||
private readonly ILogger _logger;
|
||||
private readonly RequestDelegate _next;
|
||||
|
||||
public ApiKeyMiddleware(RequestDelegate next, ApiSecurityConfiguration apiSecurityConfiguration, ILoggerFactory loggerFactory)
|
||||
{
|
||||
_next = next;
|
||||
_apiSecurityConfiguration = apiSecurityConfiguration;
|
||||
_logger = loggerFactory.CreateLogger(GetType().Name);
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
var displayUrl = context.Request.GetDisplayUrl();
|
||||
if (_apiSecurityConfiguration.ExternalApiKeySet.Any())
|
||||
{
|
||||
if (!context.Request.Headers.TryGetValue(HeaderKeys.ApiKey, out var extractedApiKey))
|
||||
{
|
||||
switch (_apiSecurityConfiguration.ApiKeyAuthenticationFailureBehavior)
|
||||
{
|
||||
case ApiKeyAuthenticationFailureBehavior.Warn:
|
||||
_logger.LogWarning(AuthMessages.NotProvidedMessage(HeaderKeys.ApiKey));
|
||||
break;
|
||||
case ApiKeyAuthenticationFailureBehavior.Error:
|
||||
_logger.LogError(AuthMessages.NotProvidedMessage(HeaderKeys.ApiKey));
|
||||
context.Response.StatusCode = 401;
|
||||
await context.Response.WriteAsync(AuthMessages.NotProvidedMessage(HeaderKeys.ApiKey));
|
||||
return;
|
||||
default:
|
||||
throw new Exception("Invalid ApiKeyAuthenticationFailureBehavior Value");
|
||||
}
|
||||
}
|
||||
|
||||
if (!context.Request.Headers.TryGetValue(HeaderKeys.Client, out var extractedClient))
|
||||
{
|
||||
switch (_apiSecurityConfiguration.ApiKeyAuthenticationFailureBehavior)
|
||||
{
|
||||
case ApiKeyAuthenticationFailureBehavior.Warn:
|
||||
_logger.LogWarning(AuthMessages.NotProvidedMessage(HeaderKeys.Client));
|
||||
break;
|
||||
case ApiKeyAuthenticationFailureBehavior.Error:
|
||||
_logger.LogError(AuthMessages.NotProvidedMessage(HeaderKeys.Client));
|
||||
context.Response.StatusCode = 401;
|
||||
await context.Response.WriteAsync(AuthMessages.NotProvidedMessage(HeaderKeys.Client));
|
||||
return;
|
||||
default:
|
||||
throw new Exception("Invalid ApiKeyAuthenticationFailureBehavior Value");
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(extractedClient))
|
||||
{
|
||||
if (_apiSecurityConfiguration.ExternalApiKeySet.TryGetValue(extractedClient, out var apiKey))
|
||||
{
|
||||
if (!apiKey.Equals(extractedApiKey))
|
||||
{
|
||||
switch (_apiSecurityConfiguration.ApiKeyAuthenticationFailureBehavior)
|
||||
{
|
||||
case ApiKeyAuthenticationFailureBehavior.Warn:
|
||||
_logger.LogWarning(AuthMessages.AuthFailureMessage(extractedClient));
|
||||
break;
|
||||
case ApiKeyAuthenticationFailureBehavior.Error:
|
||||
_logger.LogError(AuthMessages.AuthFailureMessage(extractedClient));
|
||||
context.Response.StatusCode = 401;
|
||||
await context.Response.WriteAsync(AuthMessages.AuthFailureMessage(extractedClient));
|
||||
return;
|
||||
default:
|
||||
throw new Exception("Invalid ApiKeyAuthenticationFailureBehavior Value");
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation($"Valid auth received for Client: {extractedClient}");
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (_apiSecurityConfiguration.ApiKeyAuthenticationFailureBehavior)
|
||||
{
|
||||
case ApiKeyAuthenticationFailureBehavior.Warn:
|
||||
_logger.LogWarning(AuthMessages.ClientNotFoundMessage());
|
||||
break;
|
||||
case ApiKeyAuthenticationFailureBehavior.Error:
|
||||
_logger.LogError(AuthMessages.ClientNotFoundMessage());
|
||||
context.Response.StatusCode = 401;
|
||||
await context.Response.WriteAsync(AuthMessages.ClientNotFoundMessage());
|
||||
return;
|
||||
default:
|
||||
throw new Exception("Invalid ApiKeyAuthenticationFailureBehavior Value");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (_apiSecurityConfiguration.ApiKeyAuthenticationFailureBehavior)
|
||||
{
|
||||
case ApiKeyAuthenticationFailureBehavior.Warn:
|
||||
_logger.LogWarning(AuthMessages.EmptyKeySetMessage("ExternalApiKeySet"));
|
||||
break;
|
||||
case ApiKeyAuthenticationFailureBehavior.Error:
|
||||
_logger.LogError(AuthMessages.EmptyKeySetMessage("ExternalApiKeySet"));
|
||||
context.Response.StatusCode = 401;
|
||||
await context.Response.WriteAsync(AuthMessages.EmptyKeySetMessage("ExternalApiKeySet"));
|
||||
return;
|
||||
default:
|
||||
throw new Exception("Invalid ApiKeyAuthenticationFailureBehavior Value");
|
||||
}
|
||||
}
|
||||
|
||||
await _next(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using LCU.Library.Internal.ApiSecurity.Enums;
|
||||
|
||||
namespace LCU.Library.Internal.ApiSecurity.Models;
|
||||
|
||||
public class ApiSecurityConfiguration
|
||||
{
|
||||
public ApiKeyAuthenticationFailureBehavior ApiKeyAuthenticationFailureBehavior { get; set; }
|
||||
|
||||
[Required]
|
||||
public Dictionary<string, string> ExternalApiKeySet { get; set; }
|
||||
|
||||
[Required]
|
||||
public Dictionary<string, string> InternalApiKeySet { get; set; }
|
||||
|
||||
public ApiSecurityConfiguration(ApiKeyAuthenticationFailureBehavior apiKeyAuthenticationFailureBehavior = ApiKeyAuthenticationFailureBehavior.Error)
|
||||
{
|
||||
ApiKeyAuthenticationFailureBehavior = apiKeyAuthenticationFailureBehavior;
|
||||
ExternalApiKeySet = new Dictionary<string, string>();
|
||||
InternalApiKeySet = new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
public string ExtractApiKey(string clientKeyValue, ApiKeySetName keySetName)
|
||||
{
|
||||
string extractedApiKey = "";
|
||||
|
||||
if (keySetName == ApiKeySetName.InternalKeySet && !InternalApiKeySet.TryGetValue(clientKeyValue, out extractedApiKey))
|
||||
{
|
||||
throw new Exception("Your Internal Api Key is missing");
|
||||
}
|
||||
|
||||
if (keySetName == ApiKeySetName.ExternalKeySet && !ExternalApiKeySet.TryGetValue(clientKeyValue, out extractedApiKey))
|
||||
{
|
||||
throw new Exception("Your External Api Key is missing");
|
||||
}
|
||||
return extractedApiKey;
|
||||
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using LCU.Library.Internal.ApiSecurity.Models;
|
||||
|
||||
namespace LCU.Library.Internal.ApiSecurity;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds required API Security services.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection that API Security services are added to.</param>
|
||||
/// <returns>A <see cref="IApiSecurityBuilder"/> that can be used to customize API Security functionality.</returns>
|
||||
public static IApiSecurityBuilder AddApiKeySecurity(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.AddLogging();
|
||||
|
||||
//TODO: Move this functionality from binding a config to making a call to an API Security API, only hold config in memory instead of file system
|
||||
//This is less of a concern for containerized apps but still not ideal since the credentials will still be on the containers file system
|
||||
var apiSecurityConfiguration = new ApiSecurityConfiguration();
|
||||
configuration.GetSection("ApiSecurityConfiguration").Bind(apiSecurityConfiguration);
|
||||
services.AddSingleton(apiSecurityConfiguration);
|
||||
|
||||
return new ApiSecurityBuilder(services);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user