Compare commits
70 Commits
92a7b959ab
...
feat/Infra
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb9449d701 | ||
|
|
72f735272f | ||
|
|
304f5b7b4c | ||
|
|
0238310290 | ||
|
|
3ac0501231 | ||
|
|
db8a560805 | ||
|
|
e67361bfe1 | ||
|
|
91594e80bf | ||
|
|
8d98159ba8 | ||
|
|
f1f5b9e16d | ||
|
|
3955dede16 | ||
|
|
65e834784a | ||
|
|
3c1bbc1151 | ||
|
|
5465fe5b49 | ||
|
|
85787e7054 | ||
|
|
c955220310 | ||
|
|
7d2098092a | ||
|
|
e3b9d2971b | ||
|
|
3a604ede88 | ||
|
|
476c86ff0a | ||
|
|
9376fcff86 | ||
|
|
06df97597e | ||
|
|
266d03e0a1 | ||
|
|
e752c6f6ab | ||
|
|
561a751de4 | ||
|
|
35050d65a8 | ||
|
|
cf9041980d | ||
|
|
cf2ee73ca1 | ||
|
|
52f6dc161e | ||
|
|
7670898e24 | ||
|
|
f5b202c325 | ||
|
|
fffbdf752f | ||
|
|
6916e169b1 | ||
|
|
0c529b199b | ||
|
|
5427a9722d | ||
|
|
bccfae59cd | ||
|
|
4c55ecb427 | ||
|
|
a7e4291e42 | ||
|
|
352b59dfdf | ||
|
|
1b793e2b75 | ||
|
|
72603f836c | ||
|
|
162da9a16c | ||
|
|
c0524cbca2 | ||
|
|
51c1f408d7 | ||
|
|
a034ecdb1b | ||
|
|
f4a9c5c57a | ||
|
|
e15f32ae56 | ||
|
|
44d4bb9c23 | ||
|
|
a0c5144c28 | ||
|
|
dda9b40bd3 | ||
|
|
de89185b43 | ||
|
|
a94c7249e4 | ||
|
|
3f8145e6dc | ||
|
|
f3f65415e1 | ||
|
|
192a93d153 | ||
|
|
9ec9bcd474 | ||
|
|
7dd8271f4a | ||
|
|
b8de148c52 | ||
|
|
0523308083 | ||
|
|
875692b578 | ||
|
|
973a5f1023 | ||
|
|
8498dc0456 | ||
|
|
6a12ad77ec | ||
|
|
144fe86987 | ||
|
|
528a346883 | ||
|
|
a7fb97de4a | ||
|
|
c3dbc1ec54 | ||
|
|
a86989479f | ||
|
|
53427282c5 | ||
|
|
90c85814b0 |
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net7.0;net8.0</TargetFrameworks>
|
||||
<TargetFrameworks>net7.0;net8.0;net9.0</TargetFrameworks>
|
||||
<IsPackable>true</IsPackable>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace DigitalData.Core.Abstractions.Security.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a unique security context that identifies an issuer and an audience.
|
||||
/// </summary>
|
||||
public interface IUniqueSecurityContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the issuer identifier for this security context.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The issuer typically represents the entity that issues a token or a cryptographic key.
|
||||
/// </remarks>
|
||||
string Issuer { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the audience identifier for this security context.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The audience typically represents the intended recipient or target of a token or cryptographic operation.
|
||||
/// </remarks>
|
||||
string Audience { get; }
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net7.0;net8.0</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<PackageId>DigitalData.Core.Abstractions.Security</PackageId>
|
||||
<Version>1.0.1</Version>
|
||||
<AssemblyVersion>1.0.1</AssemblyVersion>
|
||||
<FileVersion>1.0.1</FileVersion>
|
||||
<Authors>Digital Data GmbH</Authors>
|
||||
<Company>Digital Data GmbH</Company>
|
||||
<Product>Digital Data GmbH</Product>
|
||||
<Copyright>Copyright 2025</Copyright>
|
||||
<PackageProjectUrl>http://git.dd:3000/AppStd/WebCoreModules.git</PackageProjectUrl>
|
||||
<PackageIcon>core_icon.png</PackageIcon>
|
||||
<Description>This package defines the foundational abstractions for implementing security functionalities within the DigitalData.Core ecosystem. It provides interfaces and base classes for encryption, decryption, and secure authentication mechanisms. Designed to ensure flexibility and consistency, it enables seamless integration with various security implementations, such as RSA-based encryption and JWT handling.</Description>
|
||||
<PackageTags>digital data core security abstractions</PackageTags>
|
||||
<RepositoryUrl>http://git.dd:3000/AppStd/WebCoreModules.git</RepositoryUrl>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\..\nuget-package-icons\core_icon.png">
|
||||
<Pack>True</Pack>
|
||||
<PackagePath>\</PackagePath>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.7.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,64 @@
|
||||
using DigitalData.Core.Abstractions.Security.Common;
|
||||
using DigitalData.Core.Abstractions.Security.Key;
|
||||
using DigitalData.Core.Abstractions.Security.Services;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System.Text;
|
||||
|
||||
namespace DigitalData.Core.Abstractions.Security.Extensions;
|
||||
|
||||
public static class SecurityExtensions
|
||||
{
|
||||
#region Unique Security Context
|
||||
public static IEnumerable<TUniqueSecurityContext> GetByIssuer<TUniqueSecurityContext>(this IEnumerable<TUniqueSecurityContext> contextes, string issuer) where TUniqueSecurityContext : IUniqueSecurityContext
|
||||
=> contextes.Where(c => c.Issuer == issuer);
|
||||
|
||||
public static IEnumerable<TUniqueSecurityContext> GetByAudience<TUniqueSecurityContext>(this IEnumerable<TUniqueSecurityContext> contextes, string audience) where TUniqueSecurityContext : IUniqueSecurityContext
|
||||
=> contextes.Where(c => c.Audience == audience);
|
||||
|
||||
public static TUniqueSecurityContext Get<TUniqueSecurityContext>(this IEnumerable<TUniqueSecurityContext> contextes, string issuer, string audience) where TUniqueSecurityContext : IUniqueSecurityContext
|
||||
=> contextes.Where(c => c.Issuer == issuer && c.Audience == audience).SingleOrDefault()
|
||||
?? throw new InvalidOperationException($"Exactly one {typeof(TUniqueSecurityContext).Name} must exist with Issuer: '{issuer}' and Audience: '{audience}'.");
|
||||
|
||||
public static bool TryGet<TUniqueSecurityContext>(this IEnumerable<TUniqueSecurityContext> contextes, string issuer, string audience, out TUniqueSecurityContext context) where TUniqueSecurityContext : IUniqueSecurityContext
|
||||
{
|
||||
#pragma warning disable CS8601 // Possible null reference assignment.
|
||||
context = contextes.SingleOrDefault(c => c.Issuer == issuer && c.Audience == audience);
|
||||
#pragma warning restore CS8601 // Possible null reference assignment.
|
||||
return context is not null;
|
||||
}
|
||||
|
||||
public static TUniqueSecurityContext Match<TUniqueSecurityContext>(this IEnumerable<TUniqueSecurityContext> contextes, IUniqueSecurityContext lookupContext) where TUniqueSecurityContext : IUniqueSecurityContext
|
||||
=> contextes.Get(lookupContext.Issuer, lookupContext.Audience);
|
||||
|
||||
public static bool TryMatch<TUniqueSecurityContext>(this IEnumerable<TUniqueSecurityContext> contextes, IUniqueSecurityContext lookupContext, out TUniqueSecurityContext context) where TUniqueSecurityContext : IUniqueSecurityContext
|
||||
=> contextes.TryGet(lookupContext.Issuer, lookupContext.Audience, out context);
|
||||
#endregion Unique Security Context
|
||||
|
||||
#region De/serilization
|
||||
internal static byte[] Base64ToByte(this string base64String) => Convert.FromBase64String(base64String);
|
||||
|
||||
internal static string BytesToString(this byte[] bytes) => Encoding.UTF8.GetString(bytes);
|
||||
|
||||
internal static string ToBase64String(this byte[] bytes) => Convert.ToBase64String(bytes);
|
||||
|
||||
internal static byte[] ToBytes(this string str) => Encoding.UTF8.GetBytes(str);
|
||||
|
||||
public static string Decrypt(this IAsymmetricDecryptor decryptor, string data) => decryptor
|
||||
.Decrypt(data.Base64ToByte()).BytesToString();
|
||||
#endregion De/serilization
|
||||
|
||||
#region Asymmetric Encryptor
|
||||
public static string Encrypt(this IAsymmetricEncryptor encryptor, string data) => encryptor.Encrypt(data.ToBytes()).ToBase64String();
|
||||
#endregion Asymmetric Encryptor
|
||||
|
||||
#region Jwt Signature Handler
|
||||
public static string WriteToken<TPrincipal>(this IJwtSignatureHandler<TPrincipal> handler, SecurityTokenDescriptor descriptor)
|
||||
=> handler.WriteToken(handler.CreateToken(descriptor));
|
||||
|
||||
public static string WriteToken<TPrincipal>(this IJwtSignatureHandler<TPrincipal> handler, TPrincipal subject, IAsymmetricTokenDescriptor descriptor)
|
||||
=> handler.WriteToken(handler.CreateToken(subject: subject, descriptor: descriptor));
|
||||
|
||||
public static string WriteToken<TPrincipal>(this IJwtSignatureHandler<TPrincipal> handler, TPrincipal subject, string issuer, string audience)
|
||||
=> handler.WriteToken(handler.CreateToken(subject: subject, issuer: issuer, audience: audience));
|
||||
#endregion Jwt Signature Handler
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace DigitalData.Core.Abstractions.Security.Key;
|
||||
|
||||
public interface IAsymmetricDecryptor : IAsymmetricPrivateKey
|
||||
{
|
||||
byte[] Decrypt(byte[] data);
|
||||
|
||||
IAsymmetricEncryptor Encryptor { get; }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace DigitalData.Core.Abstractions.Security.Key;
|
||||
|
||||
public interface IAsymmetricEncryptor : IAsymmetricPublicKey
|
||||
{
|
||||
byte[] Encrypt(byte[] data);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace DigitalData.Core.Abstractions.Security.Key;
|
||||
|
||||
public interface IAsymmetricKey
|
||||
{
|
||||
string? Id { get; }
|
||||
|
||||
string Content { get; }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace DigitalData.Core.Abstractions.Security.Key;
|
||||
|
||||
public interface IAsymmetricPrivateKey : IAsymmetricKey
|
||||
{
|
||||
bool IsEncrypted { get; }
|
||||
|
||||
IAsymmetricPublicKey PublicKey { get; }
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
namespace DigitalData.Core.Abstractions.Security.Key;
|
||||
|
||||
public interface IAsymmetricPublicKey : IAsymmetricKey
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
using DigitalData.Core.Abstractions.Security.Common;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace DigitalData.Core.Abstractions.Security.Key;
|
||||
|
||||
/// <summary>
|
||||
/// Contains some information which used to create a security token. Designed to abstract <see cref="SecurityTokenDescriptor"/>
|
||||
/// </summary>
|
||||
public interface IAsymmetricTokenDescriptor : IAsymmetricPrivateKey, IUniqueSecurityContext
|
||||
{
|
||||
IAsymmetricTokenValidator Validator { get; }
|
||||
|
||||
TimeSpan Lifetime { get; init; }
|
||||
|
||||
#region SecurityTokenDescriptor Map
|
||||
/// <summary>
|
||||
/// Defines the compression algorithm that will be used to compress the JWT token payload.
|
||||
/// </summary>
|
||||
string CompressionAlgorithm { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="EncryptingCredentials"/> used to create a encrypted security token.
|
||||
/// </summary>
|
||||
EncryptingCredentials EncryptingCredentials { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value of the 'expiration' claim. This value should be in UTC.
|
||||
/// </summary>
|
||||
DateTime? Expires { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the time the security token was issued. This value should be in UTC.
|
||||
/// </summary>
|
||||
DateTime? IssuedAt { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the notbefore time for the security token. This value should be in UTC.
|
||||
/// </summary>
|
||||
DateTime? NotBefore { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the token type.
|
||||
/// <remarks> If provided, this will be added as the value for the 'typ' header parameter. In the case of a JWE, this will be added to both the inner (JWS) and the outer token (JWE) header. By default, the value used is 'JWT'.
|
||||
/// If <see cref="AdditionalHeaderClaims"/> also contains 'typ' header claim value, it will override the TokenType provided here.
|
||||
/// This value is used only for JWT tokens and not for SAML/SAML2 tokens</remarks>
|
||||
/// </summary>
|
||||
string TokenType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="Dictionary{TKey, TValue}"/> which contains any custom header claims that need to be added to the JWT token header.
|
||||
/// The 'alg', 'kid', 'x5t', 'enc', and 'zip' claims are added by default based on the <see cref="SigningCredentials"/>,
|
||||
/// <see cref="EncryptingCredentials"/>, and/or <see cref="CompressionAlgorithm"/> provided and SHOULD NOT be included in this dictionary as this
|
||||
/// will result in an exception being thrown.
|
||||
/// <remarks> These claims are only added to the outer header (in case of a JWE).</remarks>
|
||||
/// </summary>
|
||||
IDictionary<string, object> AdditionalHeaderClaims { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="Dictionary{TKey, TValue}"/> which contains any custom header claims that need to be added to the inner JWT token header.
|
||||
/// The 'alg', 'kid', 'x5t', 'enc', and 'zip' claims are added by default based on the <see cref="SigningCredentials"/>,
|
||||
/// <see cref="EncryptingCredentials"/>, and/or <see cref="CompressionAlgorithm"/> provided and SHOULD NOT be included in this dictionary as this
|
||||
/// will result in an exception being thrown.
|
||||
/// <remarks>
|
||||
/// For JsonWebTokenHandler, these claims are merged with <see cref="AdditionalHeaderClaims"/> while adding to the inner JWT header.
|
||||
/// </remarks>
|
||||
/// </summary>
|
||||
IDictionary<string, object> AdditionalInnerHeaderClaims { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="SigningCredentials"/> used to create a security token.
|
||||
/// </summary>
|
||||
SigningCredentials SigningCredentials { get; }
|
||||
#endregion SecurityTokenDescriptor
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace DigitalData.Core.Abstractions.Security.Key;
|
||||
|
||||
public interface IAsymmetricTokenValidator : IAsymmetricPublicKey
|
||||
{
|
||||
SecurityKey SecurityKey { get; }
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using System.Security.Cryptography;
|
||||
using DigitalData.Core.Abstractions.Security.Key;
|
||||
|
||||
namespace DigitalData.Core.Abstractions.Security.Services;
|
||||
|
||||
public interface IAsymmetricKeyFactory
|
||||
{
|
||||
string CreatePrivateKeyPem(int? keySizeInBits = null, bool encrypt = false);
|
||||
|
||||
string CreateEncryptedPrivateKeyPem(
|
||||
PbeEncryptionAlgorithm? pbeEncryptionAlgorithm = null,
|
||||
HashAlgorithmName? hashAlgorithmName = null,
|
||||
int? iterationCount = null,
|
||||
int? keySizeInBits = null,
|
||||
string? password = null);
|
||||
|
||||
string CreateEncryptedPrivateKeyPem(
|
||||
PbeParameters pbeParameters,
|
||||
int? keySizeInBits = null,
|
||||
string? password = null);
|
||||
|
||||
IAsymmetricDecryptor CreateDecryptor(string pem, string? issuer = null, string? audience = null, bool encrypt = false, RSAEncryptionPadding? padding = null);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using DigitalData.Core.Abstractions.Security.Key;
|
||||
|
||||
namespace DigitalData.Core.Abstractions.Security.Services;
|
||||
|
||||
public interface IAsymmetricKeyPool : IAsymmetricKeyFactory
|
||||
{
|
||||
IEnumerable<IAsymmetricDecryptor> Decryptors { get; }
|
||||
|
||||
IAsymmetricDecryptor VaultDecryptor { get; }
|
||||
|
||||
IEnumerable<IAsymmetricTokenDescriptor> TokenDescriptors { get; }
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using DigitalData.Core.Abstractions.Security.Key;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace DigitalData.Core.Abstractions.Security
|
||||
namespace DigitalData.Core.Abstractions.Security.Services
|
||||
{
|
||||
public interface IJwtSignatureHandler<TPrincipal>
|
||||
{
|
||||
@@ -13,14 +13,82 @@ namespace DigitalData.Core.Abstractions.Application
|
||||
|
||||
Dictionary<string, string> CustomSearchFilters { get; }
|
||||
|
||||
bool ValidateCredentials(string dirEntryUsername, string dirEntryPassword);
|
||||
/// <summary>
|
||||
/// Creates the connections to the server and returns a Boolean value that specifies
|
||||
/// whether the specified username and password are valid.
|
||||
/// </summary>
|
||||
/// <param name="userName">The username that is validated on the server. See the Remarks section
|
||||
/// for more information on the format of userName.</param>
|
||||
/// <param name="password">The password that is validated on the server.</param>
|
||||
/// <returns>True if the credentials are valid; otherwise, false.</returns>
|
||||
bool ValidateCredentials(string userName, string password);
|
||||
|
||||
/// <summary>
|
||||
/// Creates the connections to the server asynchronously and returns a Boolean value that specifies
|
||||
/// whether the specified username and password are valid.
|
||||
/// </summary>
|
||||
/// <param name="userName">The username that is validated on the server. See the Remarks section
|
||||
/// for more information on the format of userName.</param>
|
||||
/// <param name="password">The password that is validated on the server.</param>
|
||||
/// <returns>True if the credentials are valid; otherwise, false.</returns>
|
||||
Task<bool> ValidateCredentialsAsync(string userName, string password);
|
||||
|
||||
/// <summary>
|
||||
/// Finds all directory entries matching the specified filter.
|
||||
/// </summary>
|
||||
/// <param name="searchRoot">The search root.</param>
|
||||
/// <param name="filter">The search filter.</param>
|
||||
/// <param name="searchScope">The search scope.</param>
|
||||
/// <param name="sizeLimit">The size limit.</param>
|
||||
/// <param name="properties">The properties to load.</param>
|
||||
/// <returns>A <see cref="DataResult{T}"/> containing the results.</returns>
|
||||
DataResult<IEnumerable<ResultPropertyCollection>> FindAll(DirectoryEntry searchRoot, string filter, SearchScope searchScope = SearchScope.Subtree, int sizeLimit = 5000, params string[] properties);
|
||||
|
||||
/// <summary>
|
||||
/// Finds all directory entries matching the specified filter asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="searchRoot">The search root.</param>
|
||||
/// <param name="filter">The search filter.</param>
|
||||
/// <param name="searchScope">The search scope.</param>
|
||||
/// <param name="sizeLimit">The size limit.</param>
|
||||
/// <param name="properties">The properties to load.</param>
|
||||
/// <returns>A <see cref="DataResult{T}"/> containing the results.</returns>
|
||||
Task<DataResult<IEnumerable<ResultPropertyCollection>>> FindAllAsync(DirectoryEntry searchRoot, string filter, SearchScope searchScope = SearchScope.Subtree, int sizeLimit = 5000, params string[] properties);
|
||||
|
||||
/// <summary>
|
||||
/// Finds all directory entries matching the specified filter, using the user cache.
|
||||
/// </summary>
|
||||
/// <param name="username">The username.</param>
|
||||
/// <param name="filter">The search filter.</param>
|
||||
/// <param name="searchScope">The search scope.</param>
|
||||
/// <param name="sizeLimit">The size limit.</param>
|
||||
/// <param name="properties">The properties to load.</param>
|
||||
/// <returns>A <see cref="DataResult{T}"/> containing the results.</returns>
|
||||
DataResult<IEnumerable<ResultPropertyCollection>> FindAllByUserCache(string username, string filter, SearchScope searchScope = SearchScope.Subtree, int sizeLimit = 5000, params string[] properties);
|
||||
|
||||
/// <summary>
|
||||
/// Finds all directory entries matching the specified filter asynchronously, using the user cache.
|
||||
/// </summary>
|
||||
/// <param name="username">The username.</param>
|
||||
/// <param name="filter">The search filter.</param>
|
||||
/// <param name="searchScope">The search scope.</param>
|
||||
/// <param name="sizeLimit">The size limit.</param>
|
||||
/// <param name="properties">The properties to load.</param>
|
||||
/// <returns>A <see cref="DataResult{T}"/> containing the results.</returns>
|
||||
Task<DataResult<IEnumerable<ResultPropertyCollection>>> FindAllByUserCacheAsync(string username, string filter, SearchScope searchScope = SearchScope.Subtree, int sizeLimit = 5000, params string[] properties);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the search root in the cache.
|
||||
/// </summary>
|
||||
/// <param name="username">The directory entry username.</param>
|
||||
/// <param name="password">The directory entry password.</param>
|
||||
void SetSearchRootCache(string username, string password);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the search root from the cache.
|
||||
/// </summary>
|
||||
/// <param name="username">The directory entry username.</param>
|
||||
/// <returns>The cached <see cref="DirectoryEntry"/> if found; otherwise, null.</returns>
|
||||
DirectoryEntry? GetSearchRootCache(string username);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net7.0;net8.0</TargetFrameworks>
|
||||
<TargetFrameworks>net7.0;net8.0;net9.0</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<!-- NuGet Package Metadata -->
|
||||
@@ -17,9 +17,9 @@
|
||||
<RepositoryUrl>http://git.dd:3000/AppStd/WebCoreModules.git</RepositoryUrl>
|
||||
<PackAsTool>False</PackAsTool>
|
||||
<PackageIcon>core_icon.png</PackageIcon>
|
||||
<Version>3.1.0</Version>
|
||||
<AssemblyVersion>3.1.0</AssemblyVersion>
|
||||
<FileVersion>3.1.0</FileVersion>
|
||||
<Version>3.4.3</Version>
|
||||
<AssemblyVersion>3.4.3</AssemblyVersion>
|
||||
<FileVersion>3.4.3</FileVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
34
DigitalData.Core.Abstractions/Infrastructure/Extensions.cs
Normal file
34
DigitalData.Core.Abstractions/Infrastructure/Extensions.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace DigitalData.Core.Abstractions.Infrastructure;
|
||||
|
||||
public static class Extensions
|
||||
{
|
||||
#region Create
|
||||
public static Task<TEntity> CreateAsync<TEntity, TDto>(this IRepository<TEntity> repository, TDto dto, CancellationToken ct = default)
|
||||
{
|
||||
var entity = repository.Mapper.Map(dto);
|
||||
return repository.CreateAsync(entity, ct);
|
||||
}
|
||||
|
||||
public static Task<IEnumerable<TEntity>> CreateAsync<TEntity, TDto>(this IRepository<TEntity> repository, IEnumerable<TDto> dtos, CancellationToken ct = default)
|
||||
{
|
||||
var entities = dtos.Select(dto => repository.Mapper.Map(dto));
|
||||
return repository.CreateAsync(entities, ct);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Read
|
||||
public static async Task<TEntity?> ReadFirstOrDefaultAsync<TEntity>(this IRepository<TEntity> repository, Expression<Func<TEntity, bool>>? expression = null)
|
||||
=> (await repository.ReadAllAsync(expression)).FirstOrDefault();
|
||||
|
||||
public static async Task<TEntity> ReadFirstAsync<TEntity>(this IRepository<TEntity> repository, Expression<Func<TEntity, bool>>? expression = null)
|
||||
=> (await repository.ReadAllAsync(expression)).First();
|
||||
|
||||
public static async Task<TEntity?> ReadSingleOrDefaultAsync<TEntity>(this IRepository<TEntity> repository, Expression<Func<TEntity, bool>>? expression = null)
|
||||
=> (await repository.ReadAllAsync(expression)).SingleOrDefault();
|
||||
|
||||
public static async Task<TEntity> ReadSingleAsync<TEntity>(this IRepository<TEntity> repository, Expression<Func<TEntity, bool>>? expression = null)
|
||||
=> (await repository.ReadAllAsync(expression)).Single();
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
namespace DigitalData.Core.Abstractions.Infrastructure
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines methods for mapping between entities and Data Transfer Objects (DTOs).
|
||||
/// </summary>
|
||||
/// <typeparam name="TEntity">The type of the entity to be mapped.</typeparam>
|
||||
public interface IEntityMapper<TEntity>
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps an entity to a DTO.
|
||||
/// </summary>
|
||||
/// <typeparam name="TDto">The type of the DTO to map to.</typeparam>
|
||||
/// <param name="entity">The entity to be mapped.</param>
|
||||
/// <returns>The mapped DTO.</returns>
|
||||
TDto Map<TDto>(TEntity entity);
|
||||
|
||||
/// <summary>
|
||||
/// Maps an entity list to a DTO list.
|
||||
/// </summary>
|
||||
/// <typeparam name="TDto">The type of the DTO to map to.</typeparam>
|
||||
/// <param name="entities">The entity list to be mapped.</param>
|
||||
/// <returns>The mapped DTO list.</returns>
|
||||
IEnumerable<TDto> Map<TDto>(IEnumerable<TEntity> entities);
|
||||
|
||||
/// <summary>
|
||||
/// Maps a DTO to an entity.
|
||||
/// </summary>
|
||||
/// <typeparam name="TDto">The type of the DTO to be mapped.</typeparam>
|
||||
/// <param name="dto">The DTO to be mapped.</param>
|
||||
/// <returns>The mapped entity.</returns>
|
||||
TEntity Map<TDto>(TDto dto);
|
||||
|
||||
/// <summary>
|
||||
/// Maps a DTO list to an entity list.
|
||||
/// </summary>
|
||||
/// <typeparam name="TDto">The type of the DTO to be mapped.</typeparam>
|
||||
/// <param name="dtos">The DTO list to be mapped.</param>
|
||||
/// <returns>The mapped entity list.</returns>
|
||||
IEnumerable<TEntity> Map<TDto>(IEnumerable<TDto> dtos);
|
||||
|
||||
/// <summary>
|
||||
/// Maps a DTO to an existing entity.
|
||||
/// </summary>
|
||||
/// <typeparam name="TDto">The type of the DTO to be mapped.</typeparam>
|
||||
/// <param name="dto">The DTO to be mapped.</param>
|
||||
/// <param name="entity">The existing entity to be updated with the mapped values.</param>
|
||||
/// <returns>The updated entity.</returns>
|
||||
TEntity Map<TDto>(TDto dto, TEntity entity);
|
||||
}
|
||||
}
|
||||
24
DigitalData.Core.Abstractions/Infrastructure/IRepository.cs
Normal file
24
DigitalData.Core.Abstractions/Infrastructure/IRepository.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace DigitalData.Core.Abstractions.Infrastructure;
|
||||
|
||||
public interface IRepository<TEntity>
|
||||
{
|
||||
public IEntityMapper<TEntity> Mapper { get; }
|
||||
|
||||
public Task<TEntity> CreateAsync(TEntity entity, CancellationToken ct = default);
|
||||
|
||||
public Task<IEnumerable<TEntity>> CreateAsync(IEnumerable<TEntity> entities, CancellationToken ct = default);
|
||||
|
||||
public Task<IEnumerable<TEntity>> ReadAllAsync(Expression<Func<TEntity, bool>>? expression = null, CancellationToken ct = default);
|
||||
|
||||
public Task<TEntity?> ReadOrDefaultAsync(Expression<Func<TEntity, bool>> expression, bool single = true, CancellationToken ct = default);
|
||||
|
||||
public Task<IEnumerable<TDto>> ReadAllAsync<TDto>(Expression<Func<TEntity, bool>>? expression = null, CancellationToken ct = default);
|
||||
|
||||
public Task<TDto?> ReadOrDefaultAsync<TDto>(Expression<Func<TEntity, bool>> expression, bool single = true, CancellationToken ct = default);
|
||||
|
||||
public Task UpdateAsync<TDto>(TDto dto, Expression<Func<TEntity, bool>> expression, CancellationToken ct = default);
|
||||
|
||||
public Task DeleteAsync(Expression<Func<TEntity, bool>> expression, CancellationToken ct = default);
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
namespace DigitalData.Core.Abstractions.Security
|
||||
{
|
||||
public interface IAsymmetricDecryptor : IAsymmetricPrivateKey
|
||||
{
|
||||
byte[] Decrypt(byte[] data);
|
||||
|
||||
IAsymmetricEncryptor Encryptor { get; }
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace DigitalData.Core.Abstractions.Security
|
||||
{
|
||||
public interface IAsymmetricEncryptor : IAsymmetricPublicKey
|
||||
{
|
||||
byte[] Encrypt(byte[] data);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
namespace DigitalData.Core.Abstractions.Security
|
||||
{
|
||||
public interface IAsymmetricKey
|
||||
{
|
||||
string? Id { get; }
|
||||
|
||||
string Content { get; }
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace DigitalData.Core.Abstractions.Security
|
||||
{
|
||||
public interface IAsymmetricKeyFactory
|
||||
{
|
||||
string CreatePrivateKeyPem(int? keySizeInBits = null, bool encrypt = false);
|
||||
|
||||
string CreateEncryptedPrivateKeyPem(
|
||||
PbeEncryptionAlgorithm? pbeEncryptionAlgorithm = null,
|
||||
HashAlgorithmName? hashAlgorithmName = null,
|
||||
int? iterationCount = null,
|
||||
int? keySizeInBits = null,
|
||||
string? password = null);
|
||||
|
||||
string CreateEncryptedPrivateKeyPem(
|
||||
PbeParameters pbeParameters,
|
||||
int? keySizeInBits = null,
|
||||
string? password = null);
|
||||
|
||||
IAsymmetricDecryptor CreateDecryptor(string pem, string? issuer = null, string? audience = null, bool encrypt = false, RSAEncryptionPadding? padding = null);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
namespace DigitalData.Core.Abstractions.Security
|
||||
{
|
||||
public interface IAsymmetricPrivateKey : IAsymmetricKey
|
||||
{
|
||||
bool IsEncrypted { get; }
|
||||
|
||||
IAsymmetricPublicKey PublicKey { get; }
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
namespace DigitalData.Core.Abstractions.Security
|
||||
{
|
||||
public interface IAsymmetricPublicKey : IAsymmetricKey
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace DigitalData.Core.Abstractions.Security
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains some information which used to create a security token. Designed to abstract <see cref="SecurityTokenDescriptor"/>
|
||||
/// </summary>
|
||||
public interface IAsymmetricTokenDescriptor : IAsymmetricPrivateKey, IUniqueSecurityContext
|
||||
{
|
||||
IAsymmetricTokenValidator Validator { get; }
|
||||
|
||||
TimeSpan Lifetime { get; init; }
|
||||
|
||||
#region SecurityTokenDescriptor Map
|
||||
/// <summary>
|
||||
/// Defines the compression algorithm that will be used to compress the JWT token payload.
|
||||
/// </summary>
|
||||
string CompressionAlgorithm { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="EncryptingCredentials"/> used to create a encrypted security token.
|
||||
/// </summary>
|
||||
EncryptingCredentials EncryptingCredentials { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value of the 'expiration' claim. This value should be in UTC.
|
||||
/// </summary>
|
||||
DateTime? Expires { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the time the security token was issued. This value should be in UTC.
|
||||
/// </summary>
|
||||
DateTime? IssuedAt { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the notbefore time for the security token. This value should be in UTC.
|
||||
/// </summary>
|
||||
DateTime? NotBefore { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the token type.
|
||||
/// <remarks> If provided, this will be added as the value for the 'typ' header parameter. In the case of a JWE, this will be added to both the inner (JWS) and the outer token (JWE) header. By default, the value used is 'JWT'.
|
||||
/// If <see cref="AdditionalHeaderClaims"/> also contains 'typ' header claim value, it will override the TokenType provided here.
|
||||
/// This value is used only for JWT tokens and not for SAML/SAML2 tokens</remarks>
|
||||
/// </summary>
|
||||
string TokenType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="Dictionary{TKey, TValue}"/> which contains any custom header claims that need to be added to the JWT token header.
|
||||
/// The 'alg', 'kid', 'x5t', 'enc', and 'zip' claims are added by default based on the <see cref="SigningCredentials"/>,
|
||||
/// <see cref="EncryptingCredentials"/>, and/or <see cref="CompressionAlgorithm"/> provided and SHOULD NOT be included in this dictionary as this
|
||||
/// will result in an exception being thrown.
|
||||
/// <remarks> These claims are only added to the outer header (in case of a JWE).</remarks>
|
||||
/// </summary>
|
||||
IDictionary<string, object> AdditionalHeaderClaims { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="Dictionary{TKey, TValue}"/> which contains any custom header claims that need to be added to the inner JWT token header.
|
||||
/// The 'alg', 'kid', 'x5t', 'enc', and 'zip' claims are added by default based on the <see cref="SigningCredentials"/>,
|
||||
/// <see cref="EncryptingCredentials"/>, and/or <see cref="CompressionAlgorithm"/> provided and SHOULD NOT be included in this dictionary as this
|
||||
/// will result in an exception being thrown.
|
||||
/// <remarks>
|
||||
/// For JsonWebTokenHandler, these claims are merged with <see cref="AdditionalHeaderClaims"/> while adding to the inner JWT header.
|
||||
/// </remarks>
|
||||
/// </summary>
|
||||
IDictionary<string, object> AdditionalInnerHeaderClaims { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="SigningCredentials"/> used to create a security token.
|
||||
/// </summary>
|
||||
SigningCredentials SigningCredentials { get; }
|
||||
#endregion SecurityTokenDescriptor
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace DigitalData.Core.Abstractions.Security
|
||||
{
|
||||
public interface IAsymmetricTokenValidator : IAsymmetricPublicKey
|
||||
{
|
||||
SecurityKey SecurityKey { get; }
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
namespace DigitalData.Core.Abstractions.Security
|
||||
{
|
||||
public interface ICryptoFactory : IAsymmetricKeyFactory
|
||||
{
|
||||
IEnumerable<IAsymmetricDecryptor> Decryptors { get; }
|
||||
|
||||
IAsymmetricDecryptor VaultDecryptor { get; }
|
||||
|
||||
IEnumerable<IAsymmetricTokenDescriptor> TokenDescriptors { get; }
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
namespace DigitalData.Core.Abstractions.Security
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a unique security context that identifies an issuer and an audience.
|
||||
/// </summary>
|
||||
public interface IUniqueSecurityContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the issuer identifier for this security context.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The issuer typically represents the entity that issues a token or a cryptographic key.
|
||||
/// </remarks>
|
||||
string Issuer { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the audience identifier for this security context.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The audience typically represents the intended recipient or target of a token or cryptographic operation.
|
||||
/// </remarks>
|
||||
string Audience { get; }
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System.Text;
|
||||
|
||||
namespace DigitalData.Core.Abstractions.Security
|
||||
{
|
||||
public static class SecurityExtensions
|
||||
{
|
||||
#region Unique Security Context
|
||||
public static IEnumerable<TUniqueSecurityContext> GetByIssuer<TUniqueSecurityContext>(this IEnumerable<TUniqueSecurityContext> contextes, string issuer) where TUniqueSecurityContext: IUniqueSecurityContext
|
||||
=> contextes.Where(c => c.Issuer == issuer);
|
||||
|
||||
public static IEnumerable<TUniqueSecurityContext> GetByAudience<TUniqueSecurityContext>(this IEnumerable<TUniqueSecurityContext> contextes, string audience) where TUniqueSecurityContext : IUniqueSecurityContext
|
||||
=> contextes.Where(c => c.Audience == audience);
|
||||
|
||||
public static TUniqueSecurityContext Get<TUniqueSecurityContext>(this IEnumerable<TUniqueSecurityContext> contextes, string issuer, string audience) where TUniqueSecurityContext : IUniqueSecurityContext
|
||||
=> contextes.Where(c => c.Issuer == issuer && c.Audience == audience).SingleOrDefault()
|
||||
?? throw new InvalidOperationException($"Exactly one {typeof(TUniqueSecurityContext).Name} must exist with Issuer: '{issuer}' and Audience: '{audience}'.");
|
||||
|
||||
public static bool TryGet<TUniqueSecurityContext>(this IEnumerable<TUniqueSecurityContext> contextes, string issuer, string audience, out TUniqueSecurityContext context) where TUniqueSecurityContext : IUniqueSecurityContext
|
||||
{
|
||||
#pragma warning disable CS8601 // Possible null reference assignment.
|
||||
context = contextes.SingleOrDefault(c => c.Issuer == issuer && c.Audience == audience);
|
||||
#pragma warning restore CS8601 // Possible null reference assignment.
|
||||
return context is not null;
|
||||
}
|
||||
|
||||
public static TUniqueSecurityContext Match<TUniqueSecurityContext>(this IEnumerable<TUniqueSecurityContext> contextes, IUniqueSecurityContext lookupContext) where TUniqueSecurityContext : IUniqueSecurityContext
|
||||
=> contextes.Get(lookupContext.Issuer, lookupContext.Audience);
|
||||
|
||||
public static bool TryMatch<TUniqueSecurityContext>(this IEnumerable<TUniqueSecurityContext> contextes, IUniqueSecurityContext lookupContext, out TUniqueSecurityContext context) where TUniqueSecurityContext : IUniqueSecurityContext
|
||||
=> contextes.TryGet(lookupContext.Issuer, lookupContext.Audience, out context);
|
||||
#endregion Unique Security Context
|
||||
|
||||
#region De/serilization
|
||||
internal static byte[] Base64ToByte(this string base64String) => Convert.FromBase64String(base64String);
|
||||
|
||||
internal static string BytesToString(this byte[] bytes) => Encoding.UTF8.GetString(bytes);
|
||||
|
||||
internal static string ToBase64String(this byte[] bytes) => Convert.ToBase64String(bytes);
|
||||
|
||||
internal static byte[] ToBytes(this string str) => System.Text.Encoding.UTF8.GetBytes(str);
|
||||
|
||||
public static string Decrypt(this IAsymmetricDecryptor decryptor, string data) => decryptor
|
||||
.Decrypt(data.Base64ToByte()).BytesToString();
|
||||
#endregion De/serilization
|
||||
|
||||
#region Asymmetric Encryptor
|
||||
public static string Encrypt(this IAsymmetricEncryptor encryptor, string data) => encryptor.Encrypt(data.ToBytes()).ToBase64String();
|
||||
#endregion Asymmetric Encryptor
|
||||
|
||||
#region Jwt Signature Handler
|
||||
public static string WriteToken<TPrincipal>(this IJwtSignatureHandler<TPrincipal> handler, SecurityTokenDescriptor descriptor)
|
||||
=> handler.WriteToken(handler.CreateToken(descriptor));
|
||||
|
||||
public static string WriteToken<TPrincipal>(this IJwtSignatureHandler<TPrincipal> handler, TPrincipal subject, IAsymmetricTokenDescriptor descriptor)
|
||||
=> handler.WriteToken(handler.CreateToken(subject: subject, descriptor: descriptor));
|
||||
|
||||
public static string WriteToken<TPrincipal>(this IJwtSignatureHandler<TPrincipal> handler, TPrincipal subject, string issuer, string audience)
|
||||
=> handler.WriteToken(handler.CreateToken(subject: subject, issuer: issuer, audience: audience));
|
||||
#endregion Jwt Signature Handler
|
||||
}
|
||||
}
|
||||
13
DigitalData.Core.Abstractions/ServiceResultExtensions.cs
Normal file
13
DigitalData.Core.Abstractions/ServiceResultExtensions.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace DigitalData.Core.Abstractions
|
||||
{
|
||||
public static class ServiceResultExtensions
|
||||
{
|
||||
public static bool Try<T>(this T? nullableResult, out T result)
|
||||
{
|
||||
#pragma warning disable CS8601 // Possible null reference assignment.
|
||||
result = nullableResult;
|
||||
#pragma warning restore CS8601 // Possible null reference assignment.
|
||||
return nullableResult is not null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net7.0;net8.0</TargetFrameworks>
|
||||
<TargetFrameworks>net7.0;net8.0;net9.0</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
|
||||
@@ -14,9 +14,9 @@
|
||||
<PackageIcon>core_icon.png</PackageIcon>
|
||||
<RepositoryUrl>http://git.dd:3000/AppStd/WebCoreModules.git</RepositoryUrl>
|
||||
<PackageTags>digital data core application clean architecture</PackageTags>
|
||||
<Version>3.1.0</Version>
|
||||
<AssemblyVersion>3.1.0</AssemblyVersion>
|
||||
<FileVersion>3.1.0</FileVersion>
|
||||
<Version>3.2.1</Version>
|
||||
<AssemblyVersion>3.2.1</AssemblyVersion>
|
||||
<FileVersion>3.2.1</FileVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -27,7 +27,6 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoMapper" Version="13.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Localization.Abstractions" Version="7.0.16" />
|
||||
@@ -38,6 +37,18 @@
|
||||
<PackageReference Include="System.Security.Cryptography.Cng" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net7.0'">
|
||||
<PackageReference Include="AutoMapper" Version="13.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
|
||||
<PackageReference Include="AutoMapper" Version="14.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
|
||||
<PackageReference Include="AutoMapper" Version="14.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\DigitalData.Core.Abstractions\DigitalData.Core.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -8,6 +8,7 @@ using Microsoft.Extensions.Options;
|
||||
|
||||
namespace DigitalData.Core.Application
|
||||
{
|
||||
//TODO: rename as DirectorySearcher
|
||||
[SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "<Pending>")]
|
||||
public class DirectorySearchService : IDirectorySearchService
|
||||
{
|
||||
@@ -45,17 +46,31 @@ namespace DigitalData.Core.Application
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the credentials of a directory entry.
|
||||
/// Creates the connections to the server and returns a Boolean value that specifies
|
||||
/// whether the specified username and password are valid.
|
||||
/// </summary>
|
||||
/// <param name="dirEntryUsername">The directory entry username.</param>
|
||||
/// <param name="dirEntryPassword">The directory entry password.</param>
|
||||
/// <param name="userName">The username that is validated on the server. See the Remarks section
|
||||
/// for more information on the format of userName.</param>
|
||||
/// <param name="password">The password that is validated on the server.</param>
|
||||
/// <returns>True if the credentials are valid; otherwise, false.</returns>
|
||||
public bool ValidateCredentials(string dirEntryUsername, string dirEntryPassword)
|
||||
public bool ValidateCredentials(string userName, string password)
|
||||
{
|
||||
using var context = new PrincipalContext(ContextType.Domain, ServerName, Root);
|
||||
return context.ValidateCredentials(dirEntryUsername, dirEntryPassword);
|
||||
return context.ValidateCredentials(userName, password);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the connections to the server asynchronously and returns a Boolean value that specifies
|
||||
/// whether the specified username and password are valid.
|
||||
/// </summary>
|
||||
/// <param name="userName">The username that is validated on the server. See the Remarks section
|
||||
/// for more information on the format of userName.</param>
|
||||
/// <param name="password">The password that is validated on the server.</param>
|
||||
/// <returns>True if the credentials are valid; otherwise, false.</returns>
|
||||
public Task<bool> ValidateCredentialsAsync(string userName, string password) => Task.Run(()
|
||||
=> ValidateCredentials(userName, password));
|
||||
|
||||
//TODO: remove unnecessary DataResult
|
||||
/// <summary>
|
||||
/// Finds all directory entries matching the specified filter.
|
||||
/// </summary>
|
||||
@@ -69,7 +84,7 @@ namespace DigitalData.Core.Application
|
||||
{
|
||||
List<ResultPropertyCollection> list = new();
|
||||
|
||||
var searcher = new DirectorySearcher()
|
||||
using var searcher = new DirectorySearcher()
|
||||
{
|
||||
Filter = filter,
|
||||
SearchScope = searchScope,
|
||||
@@ -94,6 +109,18 @@ namespace DigitalData.Core.Application
|
||||
return Result.Success<IEnumerable<ResultPropertyCollection>>(list);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds all directory entries matching the specified filter asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="searchRoot">The search root.</param>
|
||||
/// <param name="filter">The search filter.</param>
|
||||
/// <param name="searchScope">The search scope.</param>
|
||||
/// <param name="sizeLimit">The size limit.</param>
|
||||
/// <param name="properties">The properties to load.</param>
|
||||
/// <returns>A <see cref="DataResult{T}"/> containing the results.</returns>
|
||||
public Task<DataResult<IEnumerable<ResultPropertyCollection>>> FindAllAsync(DirectoryEntry searchRoot, string filter, SearchScope searchScope = SearchScope.Subtree, int sizeLimit = 5000, params string[] properties) => Task.Run(()
|
||||
=> FindAll(searchRoot, filter, searchScope, sizeLimit, properties));
|
||||
|
||||
/// <summary>
|
||||
/// Finds all directory entries matching the specified filter, using the user cache.
|
||||
/// </summary>
|
||||
@@ -113,27 +140,39 @@ namespace DigitalData.Core.Application
|
||||
return FindAll(searchRoot, filter, searchScope, sizeLimit, properties);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds all directory entries matching the specified filter asynchronously, using the user cache.
|
||||
/// </summary>
|
||||
/// <param name="username">The username.</param>
|
||||
/// <param name="filter">The search filter.</param>
|
||||
/// <param name="searchScope">The search scope.</param>
|
||||
/// <param name="sizeLimit">The size limit.</param>
|
||||
/// <param name="properties">The properties to load.</param>
|
||||
/// <returns>A <see cref="DataResult{T}"/> containing the results.</returns>
|
||||
public Task<DataResult<IEnumerable<ResultPropertyCollection>>> FindAllByUserCacheAsync(string username, string filter, SearchScope searchScope = SearchScope.Subtree, int sizeLimit = 5000, params string[] properties) => Task.Run(()
|
||||
=> FindAllByUserCache(username, filter, searchScope, sizeLimit, properties));
|
||||
|
||||
/// <summary>
|
||||
/// Sets the search root in the cache.
|
||||
/// </summary>
|
||||
/// <param name="dirEntryUsername">The directory entry username.</param>
|
||||
/// <param name="dirEntryPassword">The directory entry password.</param>
|
||||
public void SetSearchRootCache(string dirEntryUsername, string dirEntryPassword)
|
||||
/// <param name="username">The directory entry username.</param>
|
||||
/// <param name="password">The directory entry password.</param>
|
||||
public void SetSearchRootCache(string username, string password)
|
||||
{
|
||||
if (_userCacheExpiration is DateTimeOffset cacheExpiration)
|
||||
_memoryCache.Set(key: dirEntryUsername, new DirectoryEntry(path: SearchRootPath, username: dirEntryUsername, password: dirEntryPassword), absoluteExpiration: cacheExpiration);
|
||||
_memoryCache.Set(key: username, new DirectoryEntry(path: SearchRootPath, username: username, password: password), absoluteExpiration: cacheExpiration);
|
||||
else
|
||||
_memoryCache.Set(key: dirEntryUsername, new DirectoryEntry(path: SearchRootPath, username: dirEntryUsername, password: dirEntryPassword));
|
||||
_memoryCache.Set(key: username, new DirectoryEntry(path: SearchRootPath, username: username, password: password));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the search root from the cache.
|
||||
/// </summary>
|
||||
/// <param name="dirEntryUsername">The directory entry username.</param>
|
||||
/// <param name="username">The directory entry username.</param>
|
||||
/// <returns>The cached <see cref="DirectoryEntry"/> if found; otherwise, null.</returns>
|
||||
public DirectoryEntry? GetSearchRootCache(string dirEntryUsername)
|
||||
public DirectoryEntry? GetSearchRootCache(string username)
|
||||
{
|
||||
_memoryCache.TryGetValue(dirEntryUsername, out DirectoryEntry? root);
|
||||
_memoryCache.TryGetValue(username, out DirectoryEntry? root);
|
||||
return root;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Net;
|
||||
|
||||
//TODO: Update to use IHttpClientFactory (see also: https://learn.microsoft.com/tr-tr/dotnet/core/extensions/httpclient-factory)
|
||||
namespace DigitalData.Core.Client
|
||||
{
|
||||
public static class DIExtensions
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net7.0;net8.0</TargetFrameworks>
|
||||
<TargetFrameworks>net7.0;net8.0;net9.0</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Description>This package provides HTTP client extension methods for the DigitalData.Core library, offering simplified and asynchronous methods for fetching and handling HTTP responses. It includes utility methods for sending GET requests, reading response content as text or JSON, and deserializing JSON into dynamic or strongly-typed objects using Newtonsoft.Json. These extensions facilitate efficient and easy-to-read HTTP interactions in client applications.</Description>
|
||||
|
||||
@@ -9,28 +9,36 @@ namespace DigitalData.Core.DTO
|
||||
public static class DTOExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds a single message to the result.
|
||||
/// Adds a single message to the result, if not null.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the result.</typeparam>
|
||||
/// <param name="result">The result to add the message to.</param>
|
||||
/// <param name="message">The message to add.</param>
|
||||
/// <returns>The updated result.</returns>
|
||||
public static T Message<T>(this T result, string message) where T : Result
|
||||
public static T Message<T>(this T result, string? message) where T : Result
|
||||
{
|
||||
result.Messages.Add(message);
|
||||
if(message is not null)
|
||||
result.Messages.Add(message);
|
||||
return result;
|
||||
}
|
||||
|
||||
internal static IEnumerable<T> FilterNull<T>(this IEnumerable<T?> list)
|
||||
{
|
||||
foreach (var item in list)
|
||||
if(item is not null)
|
||||
yield return item;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds multiple messages to the result.
|
||||
/// Adds multiple messages to the result, after removing nulls.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the result.</typeparam>
|
||||
/// <param name="result">The result to add the messages to.</param>
|
||||
/// <param name="messages">The messages to add.</param>
|
||||
/// <returns>The updated result.</returns>
|
||||
public static T Message<T>(this T result, params string[] messages) where T : Result
|
||||
public static T Message<T>(this T result, params string?[] messages) where T : Result
|
||||
{
|
||||
result.Messages.AddRange(messages);
|
||||
result.Messages.AddRange(messages.FilterNull());
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -41,9 +49,9 @@ namespace DigitalData.Core.DTO
|
||||
/// <param name="result">The result to add the messages to.</param>
|
||||
/// <param name="messages">The collection of messages to add.</param>
|
||||
/// <returns>The updated result.</returns>
|
||||
public static T Message<T>(this T result, IEnumerable<string> messages) where T : Result
|
||||
public static T Message<T>(this T result, IEnumerable<string?> messages) where T : Result
|
||||
{
|
||||
result.Messages.AddRange(messages);
|
||||
result.Messages.AddRange(messages.FilterNull());
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -101,13 +109,13 @@ namespace DigitalData.Core.DTO
|
||||
/// <param name="flag">The flag associated with the notice.</param>
|
||||
/// <param name="messages">The messages to add to the notice.</param>
|
||||
/// <returns>The updated result.</returns>
|
||||
public static T Notice<T>(this T result, LogLevel level, Enum flag, params string[] messages) where T : Result
|
||||
public static T Notice<T>(this T result, LogLevel level, Enum flag, params string?[] messages) where T : Result
|
||||
{
|
||||
result.Notices.Add(new Notice()
|
||||
{
|
||||
Flag = flag,
|
||||
Level = level,
|
||||
Messages = messages.ToList()
|
||||
Messages = messages.FilterNull().ToList()
|
||||
});
|
||||
return result;
|
||||
}
|
||||
@@ -126,7 +134,7 @@ namespace DigitalData.Core.DTO
|
||||
{
|
||||
Flag = null,
|
||||
Level = level,
|
||||
Messages = messages.ToList()
|
||||
Messages = messages.FilterNull().ToList()
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net7.0;net8.0</TargetFrameworks>
|
||||
<TargetFrameworks>net7.0;net8.0;net9.0</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Description>This package provides Data Transfer Object (DTO) implementations and related utilities. It includes generic result handling, DTO extension methods, cookie consent settings management, and AutoMapper integration for robust object mapping, all adhering to Clean Architecture principles to ensure separation of concerns and maintainability.</Description>
|
||||
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
|
||||
<PackageId>DigitalData.Core.DTO</PackageId>
|
||||
<Version>2.0.0.0</Version>
|
||||
<Version>2.0.1</Version>
|
||||
<Authors>Digital Data GmbH</Authors>
|
||||
<Company>Digital Data GmbH</Company>
|
||||
<Product>DigitalData.Core.DTO</Product>
|
||||
@@ -15,6 +15,8 @@
|
||||
<PackageIcon>core_icon.png</PackageIcon>
|
||||
<RepositoryUrl>http://git.dd:3000/AppStd/WebCoreModules.git</RepositoryUrl>
|
||||
<PackageTags>digital data core dto clean architecture result pattern</PackageTags>
|
||||
<AssemblyVersion>2.0.1</AssemblyVersion>
|
||||
<FileVersion>2.0.1</FileVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace DigitalData.Core.Infrastructure.AutoMapper;
|
||||
|
||||
public static class DependencyInjection
|
||||
{
|
||||
public static EntityConfigurationOptions<TEntity> UseAutoMapper<TEntity>(this EntityConfigurationOptions<TEntity> options, params Type[] typeOfDtos)
|
||||
{
|
||||
options.AddCustomMapper<EntityAutoMapper<TEntity>>(services =>
|
||||
{
|
||||
if (typeOfDtos.Length != 0)
|
||||
{
|
||||
services.AddAutoMapper(cnf =>
|
||||
{
|
||||
foreach (var typeOfDto in typeOfDtos)
|
||||
{
|
||||
cnf.CreateMap(typeof(TEntity), typeOfDto);
|
||||
cnf.CreateMap(typeOfDto, typeof(TEntity));
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
});
|
||||
return options;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net7.0;net8.0;net9.0</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
|
||||
<PackageId>DigitalData.Core.Infrastructure.AutoMapper</PackageId>
|
||||
<Version>1.0.2</Version>
|
||||
<Authors>Digital Data GmbH</Authors>
|
||||
<Company>Digital Data GmbH</Company>
|
||||
<Product>DigitalData.Core.Infrastructure.AutoMapper</Product>
|
||||
<Description>This package provides AutoMapper support for the DigitalData.Core.Infrastructure module. It includes mapping configurations and abstractions used for object-to-object mapping within the infrastructure layer.</Description>
|
||||
<Copyright>Copyright 2024</Copyright>
|
||||
<PackageIcon>core_icon.png</PackageIcon>
|
||||
<RepositoryUrl>http://git.dd:3000/AppStd/WebCoreModules.git</RepositoryUrl>
|
||||
<RepositoryType>digital data core abstractions clean architecture mapping</RepositoryType>
|
||||
<PackageTags>digital data core infrastructure clean architecture mapping</PackageTags>
|
||||
<AssemblyVersion>1.0.2</AssemblyVersion>
|
||||
<FileVersion>1.0.2</FileVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\..\nuget-package-icons\core_icon.png">
|
||||
<Pack>True</Pack>
|
||||
<PackagePath>\</PackagePath>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\DigitalData.Core.Abstractions\DigitalData.Core.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\DigitalData.Core.Infrastructure\DigitalData.Core.Infrastructure.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,24 @@
|
||||
using AutoMapper;
|
||||
using DigitalData.Core.Abstractions.Infrastructure;
|
||||
|
||||
namespace DigitalData.Core.Infrastructure.AutoMapper;
|
||||
|
||||
public class EntityAutoMapper<TEntity> : IEntityMapper<TEntity>
|
||||
{
|
||||
private readonly IMapper _rootMapper;
|
||||
|
||||
public EntityAutoMapper(IMapper rootMapper)
|
||||
{
|
||||
_rootMapper = rootMapper;
|
||||
}
|
||||
|
||||
public TDto Map<TDto>(TEntity entity) => _rootMapper.Map<TDto>(entity);
|
||||
|
||||
public IEnumerable<TDto> Map<TDto>(IEnumerable<TEntity> entities) => _rootMapper.Map<IEnumerable<TDto>>(entities);
|
||||
|
||||
public TEntity Map<TDto>(TDto dto) => _rootMapper.Map<TEntity>(dto);
|
||||
|
||||
public IEnumerable<TEntity> Map<TDto>(IEnumerable<TDto> dtos) => _rootMapper.Map<IEnumerable<TEntity>>(dtos);
|
||||
|
||||
public TEntity Map<TDto>(TDto dto, TEntity entity) => _rootMapper.Map(dto, entity);
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
using DigitalData.Core.Abstractions;
|
||||
using DigitalData.Core.Abstractions.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System.DirectoryServices;
|
||||
|
||||
namespace DigitalData.Core.Infrastructure
|
||||
{
|
||||
public static class DIExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds a CRUD repository for a specific entity type to the service collection.
|
||||
/// </summary>
|
||||
/// <typeparam name="TEntity">The entity type for which the repository is registered.</typeparam>
|
||||
/// <typeparam name="TId">The type of the entity's identifier.</typeparam>
|
||||
/// <typeparam name="TDbContext">The DbContext type used by the repository.</typeparam>
|
||||
/// <param name="services">The <see cref="IServiceCollection"/> to add the repository to.</param>
|
||||
/// <param name="configureRepository">An optional action to configure additional services for the repository.</param>
|
||||
/// <returns>The original <see cref="IServiceCollection"/> instance, allowing further configuration.</returns>
|
||||
public static IServiceCollection AddCleanCRUDRepository<TEntity, TId, TDbContext, TCRUDRepository>(this IServiceCollection services, Action<IServiceCollection>? configureRepository = null)
|
||||
where TCRUDRepository : CRUDRepository<TEntity, TId, TDbContext> where TEntity : class, IUnique<TId> where TDbContext : DbContext
|
||||
{
|
||||
services.AddScoped<ICRUDRepository<TEntity, TId>, TCRUDRepository>();
|
||||
configureRepository?.Invoke(services);
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
||||
79
DigitalData.Core.Infrastructure/DbRepository.cs
Normal file
79
DigitalData.Core.Infrastructure/DbRepository.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
using DigitalData.Core.Abstractions.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace DigitalData.Core.Infrastructure;
|
||||
|
||||
public class DbRepository<TDbContext, TEntity> : IRepository<TEntity> where TDbContext : DbContext where TEntity : class
|
||||
{
|
||||
protected internal readonly TDbContext Context;
|
||||
|
||||
protected internal readonly DbSet<TEntity> Entities;
|
||||
|
||||
public IEntityMapper<TEntity> Mapper { get; }
|
||||
|
||||
public DbRepository(TDbContext context, Func<TDbContext, DbSet<TEntity>> queryFactory, IEntityMapper<TEntity> mapper)
|
||||
{
|
||||
Context = context;
|
||||
Entities = queryFactory(context);
|
||||
Mapper = mapper;
|
||||
}
|
||||
|
||||
public virtual async Task<TEntity> CreateAsync(TEntity entity, CancellationToken ct = default)
|
||||
{
|
||||
Entities.Add(entity);
|
||||
await Context.SaveChangesAsync(ct);
|
||||
return entity;
|
||||
}
|
||||
|
||||
public virtual async Task<IEnumerable<TEntity>> CreateAsync(IEnumerable<TEntity> entities, CancellationToken ct = default)
|
||||
{
|
||||
Entities.AddRange(entities);
|
||||
await Context.SaveChangesAsync(ct);
|
||||
return entities;
|
||||
}
|
||||
|
||||
public virtual async Task<IEnumerable<TEntity>> ReadAllAsync(Expression<Func<TEntity, bool>>? expression = null, CancellationToken ct = default)
|
||||
=> expression is null
|
||||
? await Entities.AsNoTracking().ToListAsync(ct)
|
||||
: await Entities.AsNoTracking().Where(expression).ToListAsync(ct);
|
||||
|
||||
public virtual async Task<TEntity?> ReadOrDefaultAsync(Expression<Func<TEntity, bool>> expression, bool single = true, CancellationToken ct = default)
|
||||
=> single
|
||||
? await Entities.AsNoTracking().Where(expression).SingleOrDefaultAsync(ct)
|
||||
: await Entities.AsNoTracking().Where(expression).FirstOrDefaultAsync(ct);
|
||||
|
||||
public virtual async Task<IEnumerable<TDto>> ReadAllAsync<TDto>(Expression<Func<TEntity, bool>>? expression = null, CancellationToken ct = default)
|
||||
=> Mapper.Map<TDto>(await ReadAllAsync(expression, ct));
|
||||
|
||||
public virtual async Task<TDto?> ReadOrDefaultAsync<TDto>(Expression<Func<TEntity, bool>> expression, bool single = true, CancellationToken ct = default)
|
||||
{
|
||||
var entity = await ReadOrDefaultAsync(expression, single, ct);
|
||||
return entity is null ? default : Mapper.Map<TDto>(entity);
|
||||
}
|
||||
|
||||
public virtual async Task UpdateAsync<TDto>(TDto dto, Expression<Func<TEntity, bool>> expression, CancellationToken ct = default)
|
||||
{
|
||||
var entities = await Entities.Where(expression).ToListAsync(ct);
|
||||
|
||||
for (int i = entities.Count - 1; i >= 0; i--)
|
||||
{
|
||||
Mapper.Map(dto, entities[i]);
|
||||
Entities.Update(entities[i]);
|
||||
}
|
||||
|
||||
await Context.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
public virtual async Task DeleteAsync(Expression<Func<TEntity, bool>> expression, CancellationToken ct = default)
|
||||
{
|
||||
var entities = await Entities.Where(expression).ToListAsync(ct);
|
||||
|
||||
for (int i = entities.Count - 1; i >= 0; i--)
|
||||
{
|
||||
Entities.Remove(entities[i]);
|
||||
}
|
||||
|
||||
await Context.SaveChangesAsync(ct);
|
||||
}
|
||||
}
|
||||
19
DigitalData.Core.Infrastructure/DependencyInjection.cs
Normal file
19
DigitalData.Core.Infrastructure/DependencyInjection.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using DigitalData.Core.Abstractions.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace DigitalData.Core.Infrastructure;
|
||||
|
||||
public static class DependencyInjection
|
||||
{
|
||||
public static EntityConfigurationOptions<TEntity> AddDbRepository<TDbContext, TEntity>(this IServiceCollection services, Func<TDbContext, DbSet<TEntity>> queryFactory)
|
||||
where TDbContext : DbContext
|
||||
where TEntity : class
|
||||
{
|
||||
services
|
||||
.AddScoped<IRepository<TEntity>, DbRepository<TDbContext, TEntity>>()
|
||||
.AddSingleton(queryFactory);
|
||||
|
||||
return new EntityConfigurationOptions<TEntity>(services);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net7.0;net8.0</TargetFrameworks>
|
||||
<TargetFrameworks>net7.0;net8.0;net9.0</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
|
||||
<PackageId>DigitalData.Core.Infrastructure</PackageId>
|
||||
<Version>2.0.0.0</Version>
|
||||
<Version>2.0.4</Version>
|
||||
<Authors>Digital Data GmbH</Authors>
|
||||
<Company>Digital Data GmbH</Company>
|
||||
<Product>DigitalData.Core.Infrastructure</Product>
|
||||
@@ -16,6 +16,8 @@
|
||||
<RepositoryUrl>http://git.dd:3000/AppStd/WebCoreModules.git</RepositoryUrl>
|
||||
<RepositoryType>digital data core abstractions clean architecture</RepositoryType>
|
||||
<PackageTags>digital data core infrastructure clean architecture</PackageTags>
|
||||
<AssemblyVersion>2.0.4</AssemblyVersion>
|
||||
<FileVersion>2.0.4</FileVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -25,8 +27,16 @@
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.16" />
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net7.0'">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.20" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.15" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
using DigitalData.Core.Abstractions.Infrastructure;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace DigitalData.Core.Infrastructure;
|
||||
|
||||
public class EntityConfigurationOptions<TEntity>
|
||||
{
|
||||
private readonly IServiceCollection _services;
|
||||
|
||||
public EntityConfigurationOptions(IServiceCollection services)
|
||||
{
|
||||
_services = services;
|
||||
}
|
||||
|
||||
public EntityConfigurationOptions<TEntity> AddCustomMapper<TEntityMapper>(Action<IServiceCollection> configure, Func<IServiceProvider, TEntityMapper>? factory = null)
|
||||
where TEntityMapper : class, IEntityMapper<TEntity>
|
||||
{
|
||||
configure(_services);
|
||||
|
||||
if (factory is null)
|
||||
_services.AddSingleton<IEntityMapper<TEntity>, TEntityMapper>();
|
||||
else
|
||||
_services.AddSingleton(factory);
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace DigitalData.Core.Security.Config
|
||||
{
|
||||
public class ClaimDescriptor<TPrincipal>
|
||||
{
|
||||
public Func<TPrincipal, IDictionary<string, object>>? CreateClaims { get; init; }
|
||||
namespace DigitalData.Core.Security.Config;
|
||||
|
||||
public Func<TPrincipal, ClaimsIdentity>? CreateSubject { get; init; }
|
||||
}
|
||||
public class ClaimDescriptor<TPrincipal>
|
||||
{
|
||||
public Func<TPrincipal, IDictionary<string, object>>? CreateClaims { get; init; }
|
||||
|
||||
public Func<TPrincipal, ClaimsIdentity>? CreateSubject { get; init; }
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
using DigitalData.Core.Security.RSAKey;
|
||||
|
||||
namespace DigitalData.Core.Security.Config
|
||||
{
|
||||
public class CryptoFactoryParams : RSAFactoryParams
|
||||
{
|
||||
public string PemDirectory { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the separator used to concatenate the components of a file-related token string.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The resulting file-related token string is constructed as follows:
|
||||
/// <c>string.Join(FileNameSeparator, Issuer, Audience, Secret_version)</c>.
|
||||
/// If <c>Secret_version</c> is not null, it will be included in the concatenation.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// For example, if <c>FileNameSeparator = "_-_"</c>, the output might look like:
|
||||
/// <c>"Issuer_-_Audience_-_Secret_version"</c>.
|
||||
/// </example>
|
||||
public string FileNameSeparator { get; init; } = "_-_";
|
||||
|
||||
public string FileExtension { get; init; } = "pem";
|
||||
|
||||
/// <summary>
|
||||
///This is the subtext of the pem file name. For the file to be automatically renewed, the name must be assigned to change periodically. For example, by default MM/2 will be refreshed every 2 months.
|
||||
/// <br />
|
||||
/// - <see cref="StringExtensions.ToTag(DateTime, string)" /> is used when converting to tag.
|
||||
/// <br />
|
||||
/// - If the format contains the symbol “//”, the method divides the numeric value obtained from the left side of the format
|
||||
/// by one minus the numeric value obtained from the right side of the format string and adds one. For instance:
|
||||
/// <br />
|
||||
/// - If the date is 02.03.2024 and the format is "MM//2", it extracts the month (02), subtracts one (3), divides it by 2,
|
||||
/// rounds down the outgoing number (1), adds one to the number (resulting in 2).
|
||||
/// <br />
|
||||
/// - If the format does not contain "//", the method uses the default <see cref="DateTime.ToString"/> format.
|
||||
/// <br />
|
||||
/// This method provides a way to format the date based on typical or customized rules, including mathematical operations like division.
|
||||
/// </summary>
|
||||
public string DateTagFormat { get; init; } = "MM//2";
|
||||
|
||||
public IEnumerable<RSADecryptor> Decryptors { get; init; } = new List<RSADecryptor>();
|
||||
|
||||
public IEnumerable<RSATokenDescriptor> TokenDescriptors { get; init; } = new List<RSATokenDescriptor>();
|
||||
|
||||
public RSADecryptor? VaultDecryptor { get; init; }
|
||||
|
||||
public CryptoFactoryParams()
|
||||
{
|
||||
// init decryptors
|
||||
AfterCreate += () =>
|
||||
{
|
||||
// Create root folder if it does not exist
|
||||
if (!Directory.Exists(PemDirectory))
|
||||
Directory.CreateDirectory(PemDirectory);
|
||||
|
||||
var privateKeys = new List<RSAPrivateKey>();
|
||||
privateKeys.AddRange(Decryptors);
|
||||
privateKeys.AddRange(TokenDescriptors);
|
||||
if (VaultDecryptor is not null)
|
||||
privateKeys.Add(VaultDecryptor);
|
||||
|
||||
foreach (var privateKey in privateKeys)
|
||||
{
|
||||
// set default path
|
||||
if (privateKey.IsPemNull)
|
||||
{
|
||||
// file name
|
||||
var file_name_params = new List<object>();
|
||||
|
||||
if (privateKey.Id is not null)
|
||||
file_name_params.Add(privateKey.Id);
|
||||
else if (privateKey is RSATokenDescriptor descriptor)
|
||||
file_name_params.Add(descriptor.Issuer);
|
||||
|
||||
file_name_params.Add(KeySizeInBits);
|
||||
file_name_params.Add(DateTime.Now.ToTag(DateTagFormat));
|
||||
|
||||
if (privateKey.IsEncrypted)
|
||||
file_name_params.Add(Secrets.Version);
|
||||
|
||||
var file_name = $"{string.Join(FileNameSeparator, file_name_params)}.{FileExtension}";
|
||||
|
||||
var path = Path.Combine(PemDirectory, file_name);
|
||||
|
||||
if (File.Exists(path))
|
||||
privateKey.SetPem(File.ReadAllText(path));
|
||||
else
|
||||
{
|
||||
var pem = privateKey.IsEncrypted
|
||||
? Instance.RSAFactory.CreateEncryptedPrivateKeyPem(pbeParameters: PbeParameters, keySizeInBits: KeySizeInBits, password: Secrets.PBE_PASSWORD)
|
||||
: Instance.RSAFactory.CreatePrivateKeyPem(keySizeInBits: KeySizeInBits);
|
||||
|
||||
privateKey.SetPem(pem);
|
||||
|
||||
// Save file in background
|
||||
Task.Run(async () => await File.WriteAllTextAsync(path: path, pem));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
using AutoMapper;
|
||||
using DigitalData.Core.Abstractions.Security;
|
||||
using DigitalData.Core.Abstractions.Security.Key;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace DigitalData.Core.Security.Config
|
||||
namespace DigitalData.Core.Security.Config;
|
||||
|
||||
public class MappingProfile : Profile
|
||||
{
|
||||
public class MappingProfile : Profile
|
||||
public MappingProfile()
|
||||
{
|
||||
public MappingProfile()
|
||||
{
|
||||
CreateMap<IAsymmetricTokenDescriptor, SecurityTokenDescriptor>();
|
||||
}
|
||||
CreateMap<IAsymmetricTokenDescriptor, SecurityTokenDescriptor>();
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace DigitalData.Core.Security.Config
|
||||
{
|
||||
public class ParamsConfigureOptions<TParams> : IConfigureOptions<TParams> where TParams : RSAFactoryParams
|
||||
{
|
||||
public void Configure(TParams options) => options.Init();
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
using System.Reflection;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace DigitalData.Core.Security.Config
|
||||
{
|
||||
public class RSAFactoryParams : IJsonOnDeserialized
|
||||
{
|
||||
public int KeySizeInBits { get; init; } = 2048;
|
||||
|
||||
public string PbePassword { internal get; init; } = Secrets.PBE_PASSWORD;
|
||||
|
||||
public PbeEncryptionAlgorithm PbeEncryptionAlgorithm { get; init; } = PbeEncryptionAlgorithm.Aes256Cbc;
|
||||
|
||||
public HashAlgorithmName PbeHashAlgorithm { get; init; } = HashAlgorithmName.SHA256;
|
||||
|
||||
// TODO: add as json converter to IConfigurIConfiguration.Config
|
||||
public string PbeHashAlgorithmName
|
||||
{
|
||||
get => PbeHashAlgorithm.ToString();
|
||||
init => PbeHashAlgorithm = (typeof(HashAlgorithmName).GetProperty(value, BindingFlags.Public | BindingFlags.Static)?.GetValue(null) is HashAlgorithmName hashAlgorithmName)
|
||||
? hashAlgorithmName
|
||||
: new(value);
|
||||
}
|
||||
|
||||
public int PbeIterationCount { get; init; } = 100_000;
|
||||
|
||||
public string EncryptedPrivateKeyPemLabel { get; init; } = "ENCRYPTED PRIVATE KEY";
|
||||
|
||||
private PbeParameters? _pbeParameters;
|
||||
|
||||
[JsonIgnore]
|
||||
public PbeParameters PbeParameters => _pbeParameters!;
|
||||
|
||||
/// <summary>
|
||||
/// Provides a thread-safe initialization mechanism using Lazy initialization.
|
||||
/// </summary>
|
||||
private readonly Lazy<bool> _lazyInitializer;
|
||||
|
||||
public bool IsInitialized => _lazyInitializer.IsValueCreated;
|
||||
|
||||
public RSAFactoryParams()
|
||||
{
|
||||
_lazyInitializer = new(() =>
|
||||
{
|
||||
AfterCreate?.Invoke();
|
||||
return true;
|
||||
});
|
||||
|
||||
AfterCreate += () => _pbeParameters = new PbeParameters(PbeEncryptionAlgorithm, PbeHashAlgorithm, PbeIterationCount);
|
||||
}
|
||||
|
||||
protected event Action AfterCreate;
|
||||
|
||||
public void Init() => _ = _lazyInitializer.Value;
|
||||
|
||||
public void OnDeserialized() => Init();
|
||||
}
|
||||
}
|
||||
94
DigitalData.Core.Security/Config/RSAParams.cs
Normal file
94
DigitalData.Core.Security/Config/RSAParams.cs
Normal file
@@ -0,0 +1,94 @@
|
||||
using DigitalData.Core.Security.RSAKey.Auth;
|
||||
using DigitalData.Core.Security.RSAKey.Crypto;
|
||||
using System.Reflection;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace DigitalData.Core.Security.Config;
|
||||
|
||||
public class RSAParams
|
||||
{
|
||||
#region Factory Params
|
||||
public int KeySizeInBits { get; init; } = Default.KeySizeInBits;
|
||||
|
||||
public string PbePassword { internal get; init; } = Default.PbePassword;
|
||||
|
||||
public PbeEncryptionAlgorithm PbeEncryptionAlgorithm { get; init; } = Default.PbeEncryptionAlgorithm;
|
||||
|
||||
public HashAlgorithmName PbeHashAlgorithm { get; init; } = Default.PbeHashAlgorithm;
|
||||
|
||||
// TODO: add as json converter to IConfigurIConfiguration.Config
|
||||
public string PbeHashAlgorithmName
|
||||
{
|
||||
get => PbeHashAlgorithm.ToString();
|
||||
init => PbeHashAlgorithm = (typeof(HashAlgorithmName).GetProperty(value, BindingFlags.Public | BindingFlags.Static)?.GetValue(null) is HashAlgorithmName hashAlgorithmName)
|
||||
? hashAlgorithmName
|
||||
: new(value);
|
||||
}
|
||||
|
||||
public int PbeIterationCount { get; init; } = Default.PbeIterationCount;
|
||||
|
||||
public string EncryptedPrivateKeyPemLabel { get; init; } = Default.EncryptedPrivateKeyPemLabel;
|
||||
|
||||
public PbeParameters PbeParameters => new(PbeEncryptionAlgorithm, PbeHashAlgorithm, PbeIterationCount);
|
||||
|
||||
public static class Default
|
||||
{
|
||||
public static readonly int KeySizeInBits = 2048;
|
||||
|
||||
public static readonly string PbePassword = Secrets.PBE_PASSWORD;
|
||||
|
||||
public static readonly PbeEncryptionAlgorithm PbeEncryptionAlgorithm = PbeEncryptionAlgorithm.Aes256Cbc;
|
||||
|
||||
public static readonly HashAlgorithmName PbeHashAlgorithm = HashAlgorithmName.SHA256;
|
||||
|
||||
public static readonly int PbeIterationCount = 100_000;
|
||||
|
||||
public static readonly string EncryptedPrivateKeyPemLabel = "ENCRYPTED PRIVATE KEY";
|
||||
|
||||
public static readonly PbeParameters PbeParameters = new(PbeEncryptionAlgorithm, PbeHashAlgorithm, PbeIterationCount);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Pool Params
|
||||
public string PemDirectory { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the separator used to concatenate the components of a file-related token string.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The resulting file-related token string is constructed as follows:
|
||||
/// <c>string.Join(FileNameSeparator, Issuer, Audience, Secret_version)</c>.
|
||||
/// If <c>Secret_version</c> is not null, it will be included in the concatenation.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// For example, if <c>FileNameSeparator = "_-_"</c>, the output might look like:
|
||||
/// <c>"Issuer_-_Audience_-_Secret_version"</c>.
|
||||
/// </example>
|
||||
public string FileNameSeparator { get; init; } = "_-_";
|
||||
|
||||
public string FileExtension { get; init; } = "pem";
|
||||
|
||||
/// <summary>
|
||||
///This is the subtext of the pem file name. For the file to be automatically renewed, the name must be assigned to change periodically. For example, by default MM/2 will be refreshed every 2 months.
|
||||
/// <br />
|
||||
/// - <see cref="StringExtensions.ToTag(DateTime, string)" /> is used when converting to tag.
|
||||
/// <br />
|
||||
/// - If the format contains the symbol “//”, the method divides the numeric value obtained from the left side of the format
|
||||
/// by one minus the numeric value obtained from the right side of the format string and adds one. For instance:
|
||||
/// <br />
|
||||
/// - If the date is 02.03.2024 and the format is "MM//2", it extracts the month (02), subtracts one (3), divides it by 2,
|
||||
/// rounds down the outgoing number (1), adds one to the number (resulting in 2).
|
||||
/// <br />
|
||||
/// - If the format does not contain "//", the method uses the default <see cref="DateTime.ToString"/> format.
|
||||
/// <br />
|
||||
/// This method provides a way to format the date based on typical or customized rules, including mathematical operations like division.
|
||||
/// </summary>
|
||||
public string DateTagFormat { get; init; } = "MM//2";
|
||||
|
||||
public IEnumerable<RSADecryptor> Decryptors { get; init; } = new List<RSADecryptor>();
|
||||
|
||||
public IEnumerable<RSATokenDescriptor> TokenDescriptors { get; init; } = new List<RSATokenDescriptor>();
|
||||
|
||||
public RSADecryptor? VaultDecryptor { get; init; }
|
||||
#endregion
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
using DigitalData.Core.Abstractions.Security;
|
||||
using DigitalData.Core.Security.Config;
|
||||
using DigitalData.Core.Security.RSAKey;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace DigitalData.Core.Security
|
||||
{
|
||||
public class CryptoFactory : RSAFactory<CryptoFactoryParams>, ICryptoFactory, IAsymmetricKeyFactory
|
||||
{
|
||||
public IEnumerable<IAsymmetricDecryptor> Decryptors { get; }
|
||||
|
||||
/// <summary>
|
||||
/// It is a separate decryptor for permanently stored encrypted data. It is assigned to the first Default decryptor by default.
|
||||
/// </summary>
|
||||
public IAsymmetricDecryptor VaultDecryptor { get; }
|
||||
|
||||
public IEnumerable<IAsymmetricTokenDescriptor> TokenDescriptors { get; init; } = new List<IAsymmetricTokenDescriptor>();
|
||||
|
||||
public CryptoFactory(IOptions<CryptoFactoryParams> options, ILogger<CryptoFactory>? logger = null) : base(options)
|
||||
{
|
||||
logger?.LogInformation("Core.Secrets version: {Version}, Created on: {CreationDate}.", Secrets.Version, Secrets.CreationDate.ToString("dd.MM.yyyy"));
|
||||
|
||||
if (!_params.Decryptors.Any())
|
||||
throw new InvalidOperationException(
|
||||
"Any decryptor is not found. Ensure that at least one decryptor is configured in the provided parameters. " +
|
||||
"This issue typically arises if the configuration for decryptors is incomplete or missing. " +
|
||||
"Check the 'Decryptors' collection in the configuration and verify that it contains valid entries."
|
||||
);
|
||||
|
||||
Decryptors = _params.Decryptors;
|
||||
|
||||
TokenDescriptors = _params.TokenDescriptors;
|
||||
|
||||
VaultDecryptor = _params.VaultDecryptor ?? Decryptors.First();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
using DigitalData.Core.Abstractions.Security;
|
||||
using DigitalData.Core.Security.Config;
|
||||
using DigitalData.Core.Security.RSAKey;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace DigitalData.Core.Security
|
||||
{
|
||||
public static class DIExtensions
|
||||
{
|
||||
private static IServiceCollection AddParamsConfigureOptions<TParams>(this IServiceCollection services) where TParams : RSAFactoryParams
|
||||
=> services.AddSingleton<IConfigureOptions<TParams>, ParamsConfigureOptions<TParams>>();
|
||||
|
||||
private static IServiceCollection AddCryptoFactory(this IServiceCollection services) => services
|
||||
.AddParamsConfigureOptions<CryptoFactoryParams>()
|
||||
.AddAutoMapper(typeof(MappingProfile).Assembly)
|
||||
.AddSingleton<ICryptoFactory, CryptoFactory>();
|
||||
|
||||
/// <summary>
|
||||
/// Registers a custom asym crypt service with specified parameters from the given configuration section.
|
||||
/// </summary>
|
||||
/// <param name="services"></param>
|
||||
/// <param name="section"></param>
|
||||
/// <returns>The updated <see cref="IServiceCollection"/> with the RSA Factory registered.</returns>
|
||||
public static IServiceCollection AddCryptoFactory(this IServiceCollection services, IConfigurationSection section) => services
|
||||
.Configure<CryptoFactoryParams>(section)
|
||||
.AddCryptoFactory();
|
||||
|
||||
/// <summary>
|
||||
/// Registers an asym crypt service with the specified parameters from the given instance.
|
||||
/// </summary>
|
||||
/// <param name="services"></param>
|
||||
/// <returns>The updated <see cref="IServiceCollection"/> with the RSA Factory registered.</returns>
|
||||
public static IServiceCollection AddCryptoFactory(this IServiceCollection services, CryptoFactoryParams? factoryParams = null) => services
|
||||
.AddSingleton(Options.Create(factoryParams ?? new()))
|
||||
.AddCryptoFactory();
|
||||
|
||||
/// <summary>
|
||||
/// Registers a custom RSA Factory with specified parameters from the given configuration section.
|
||||
/// </summary>
|
||||
/// <param name="services"></param>
|
||||
/// <param name="section"></param>
|
||||
/// <returns>The updated <see cref="IServiceCollection"/> with the RSA Factory registered.</returns>
|
||||
public static IServiceCollection AddRSAFactory(this IServiceCollection services, IConfigurationSection section) => services
|
||||
.AddParamsConfigureOptions<RSAFactoryParams>()
|
||||
.Configure<RSAFactoryParams>(section)
|
||||
.AddSingleton<IAsymmetricKeyFactory, RSAFactory<RSAFactoryParams>>();
|
||||
|
||||
private static IServiceCollection AddClaimDescriptor<TPrincipal>(this IServiceCollection services,
|
||||
Func<TPrincipal, IDictionary<string, object>>? claimsMapper = null,
|
||||
Func<TPrincipal, ClaimsIdentity>? subjectMapper = null)
|
||||
{
|
||||
var descriptor = new ClaimDescriptor<TPrincipal>
|
||||
{
|
||||
CreateClaims = claimsMapper,
|
||||
CreateSubject = subjectMapper
|
||||
};
|
||||
|
||||
return services.AddSingleton(sp => Options.Create(descriptor));
|
||||
}
|
||||
|
||||
public static IServiceCollection AddJwtSignatureHandler<TPrincipal>(this IServiceCollection services,
|
||||
Func<TPrincipal, IDictionary<string, object>>? claimsMapper = null,
|
||||
Func<TPrincipal, ClaimsIdentity>? subjectMapper = null)
|
||||
=> services
|
||||
.AddClaimDescriptor(claimsMapper: claimsMapper, subjectMapper: subjectMapper)
|
||||
.AddSingleton<IJwtSignatureHandler<TPrincipal>, JwtSignatureHandler<TPrincipal>>();
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<PackageId>DigitalData.Core.Security</PackageId>
|
||||
<Version>1.0.0</Version>
|
||||
<Version>1.2.3</Version>
|
||||
<Company>Digital Data GmbH</Company>
|
||||
<Product>Digital Data GmbH</Product>
|
||||
<Description>This package provides RSA-based security functionalities as an implementation of the DigitalData.Core.Abstractions.Security library. It supports robust encryption and decryption operations, as well as JWT signing and validation for secure authentication and data integrity.</Description>
|
||||
@@ -15,8 +15,8 @@
|
||||
<PackageIcon>core_icon.png</PackageIcon>
|
||||
<RepositoryUrl>http://git.dd:3000/AppStd/WebCoreModules.git</RepositoryUrl>
|
||||
<PackageTags>digital data core security</PackageTags>
|
||||
<AssemblyVersion>1.0.0</AssemblyVersion>
|
||||
<FileVersion>1.0.0</FileVersion>
|
||||
<AssemblyVersion>1.2.3</AssemblyVersion>
|
||||
<FileVersion>1.2.3</FileVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -25,14 +25,23 @@
|
||||
<PackagePath>\</PackagePath>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net7.0'">
|
||||
<PackageReference Include="AutoMapper" Version="13.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="7.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
|
||||
<PackageReference Include="AutoMapper" Version="14.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\DigitalData.Core.Abstractions\DigitalData.Core.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\DigitalData.Core.Abstractions.Security\DigitalData.Core.Abstractions.Security.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
using AutoMapper;
|
||||
using DigitalData.Core.Abstractions.Security;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace DigitalData.Core.Security
|
||||
{
|
||||
internal static class Extension
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a <see cref="DateTime"/> to a formatted string based on the specified format string.
|
||||
/// <br />
|
||||
/// - If the format contains the symbol “//”, the method divides the numeric value obtained from the left side of the format
|
||||
/// by one minus the numeric value obtained from the right side of the format string and adds one. For instance:
|
||||
/// <br />
|
||||
/// - If the date is 02.03.2024 and the format is "MM//2", it extracts the month (02), subtracts one (3), divides it by 2,
|
||||
/// rounds down the outgoing number (1), adds one to the number (resulting in 2).
|
||||
/// <br />
|
||||
/// - If the format does not contain "//", the method uses the default <see cref="DateTime.ToString"/> format.
|
||||
/// <br />
|
||||
/// </summary>
|
||||
/// <param name="date">The <see cref="DateTime"/> value to be formatted.</param>
|
||||
/// <param name="format">The format string that dictates the formatting of the date. If the format includes the "//" symbol,
|
||||
/// it splits the string at "//" and divides the left-side value by the right-side value. The format string can include standard
|
||||
/// <see cref="DateTime.ToString"/> format patterns.</param>
|
||||
/// <returns>A string representation of the formatted date, or the result of the division operation if "//" is present in the format.</returns>
|
||||
/// <exception cref="ArgumentException">Thrown if the format string is invalid, such as having an incorrect number of parts after "//".</exception>
|
||||
/// <exception cref="DivideByZeroException">Thrown if the right side of the "//" contains a zero, resulting in division by zero.</exception>
|
||||
/// <exception cref="FormatException">Thrown if either the left-side or right-side value of "//" cannot be parsed as an integer.</exception>
|
||||
internal static string ToTag(this DateTime date, string format)
|
||||
{
|
||||
if (format is not null && format.Contains("//"))
|
||||
{
|
||||
var subStrings = format.Split("//");
|
||||
|
||||
if (subStrings.Length != 2)
|
||||
throw new ArgumentException($"Date tag format {format} is invalid. It must contain exactly one '//' separator.", nameof(format));
|
||||
|
||||
var formattedLeft = date.ToString(subStrings[0]);
|
||||
|
||||
if (!int.TryParse(formattedLeft, out var dateValue))
|
||||
throw new FormatException($"The left-side value ({formattedLeft}) of the format could not be parsed to an integer.");
|
||||
|
||||
if (!int.TryParse(subStrings[1], out var divisor))
|
||||
throw new FormatException($"The right-side value ({divisor}) of the format could not be parsed to an integer.");
|
||||
|
||||
if (divisor == 0)
|
||||
throw new DivideByZeroException($"Date tag format {format} includes division by zero, which is not allowed.");
|
||||
|
||||
var result = (dateValue - 1) / divisor + 1;
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
return date.ToString(format);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a <see cref="DateTime"/> to a formatted string based on the specified format string.
|
||||
/// <br />
|
||||
/// - If the format contains the symbol “//”, the method divides the numeric value obtained from the left side of the format
|
||||
/// by one minus the numeric value obtained from the right side of the format string and adds one. For instance:
|
||||
/// <br />
|
||||
/// - If the date is 02.03.2024 and the format is "MM//2", it extracts the month (02), subtracts one (3), divides it by 2,
|
||||
/// rounds down the outgoing number (1), adds one to the number (resulting in 2).
|
||||
/// <br />
|
||||
/// - If the format does not contain "//", the method uses the default <see cref="DateTime.ToString"/> format.
|
||||
/// <br />
|
||||
/// This method provides a way to format the date based on typical or customized rules, including mathematical operations like division.
|
||||
/// </summary>
|
||||
/// <param name="date">The <see cref="DateOnly"/> value to be formatted. It will convert to DateTime to use the method shared with DateTime.</param>
|
||||
/// <param name="format">The format string that dictates the formatting of the date. If the format includes the "//" symbol,
|
||||
/// it splits the string at "//" and divides the left-side value by the right-side value. The format string can include standard
|
||||
/// <see cref="DateTime.ToString"/> format patterns.</param>
|
||||
/// <returns>A string representation of the formatted date, or the result of the division operation if "//" is present in the format.</returns>
|
||||
/// <exception cref="ArgumentException">Thrown if the format string is invalid, such as having an incorrect number of parts after "//".</exception>
|
||||
/// <exception cref="DivideByZeroException">Thrown if the right side of the "//" contains a zero, resulting in division by zero.</exception>
|
||||
/// <exception cref="FormatException">Thrown if either the left-side or right-side value of "//" cannot be parsed as an integer.</exception>
|
||||
internal static string ToTag(this DateOnly date, string format) => date.ToDateTime(new()).ToTag(format);
|
||||
|
||||
/// <summary>
|
||||
/// Maps a <see cref="RSATokenDescriptor"/> to a <see cref="SecurityTokenDescriptor"/>.
|
||||
/// </summary>
|
||||
/// <param name="mapper">The <see cref="IMapper"/> instance used for mapping.</param>
|
||||
/// <param name="description">The <see cref="RSATokenDescriptor"/> instance to be mapped.</param>
|
||||
/// <returns>A <see cref="SecurityTokenDescriptor"/> instance populated with the mapped values.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown if <paramref name="mapper"/> or <paramref name="description"/> is <c>null</c>.</exception>
|
||||
internal static SecurityTokenDescriptor Map(this IMapper mapper, IAsymmetricTokenDescriptor description)
|
||||
=> mapper.Map(description, new SecurityTokenDescriptor());
|
||||
}
|
||||
}
|
||||
49
DigitalData.Core.Security/Extensions/DIExtensions.cs
Normal file
49
DigitalData.Core.Security/Extensions/DIExtensions.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using DigitalData.Core.Abstractions.Security.Services;
|
||||
using DigitalData.Core.Security.Config;
|
||||
using DigitalData.Core.Security.Services;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace DigitalData.Core.Security.Extensions;
|
||||
|
||||
public static class DIExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers a custom asym crypt service with specified parameters from the given configuration section.
|
||||
/// </summary>
|
||||
/// <param name="services"></param>
|
||||
/// <param name="configuration"></param>
|
||||
/// <returns>The updated <see cref="IServiceCollection"/> with the RSA Factory registered.</returns>
|
||||
public static IServiceCollection AddRSAPool(this IServiceCollection services, IConfiguration configuration) => services
|
||||
.Configure<RSAParams>(configuration)
|
||||
.AddAutoMapper(typeof(MappingProfile).Assembly)
|
||||
.AddSingleton<IAsymmetricKeyPool, RSAPool>()
|
||||
.AddSingleton<IAsymmetricKeyFactory, RSAFactory>()
|
||||
.AddHostedService<PemFileInitalizer>();
|
||||
|
||||
/// <summary>
|
||||
/// Registers a custom asym crypt service with specified parameters from the given configuration section.
|
||||
/// </summary>
|
||||
/// <param name="services"></param>
|
||||
/// <param name="rsaParams"></param>
|
||||
/// <returns>The updated <see cref="IServiceCollection"/> with the RSA Factory registered.</returns>
|
||||
public static IServiceCollection AddRSAPool(this IServiceCollection services, RSAParams rsaParams) => services
|
||||
.AddSingleton(Options.Create(rsaParams))
|
||||
.AddAutoMapper(typeof(MappingProfile).Assembly)
|
||||
.AddSingleton<IAsymmetricKeyPool, RSAPool>()
|
||||
.AddSingleton<IAsymmetricKeyFactory, RSAFactory>()
|
||||
.AddHostedService<PemFileInitalizer>();
|
||||
|
||||
public static IServiceCollection AddJwtSignatureHandler<TPrincipal>(this IServiceCollection services,
|
||||
Func<TPrincipal, IDictionary<string, object>>? claimsMapper = null,
|
||||
Func<TPrincipal, ClaimsIdentity>? subjectMapper = null)
|
||||
=> services
|
||||
.AddSingleton<IJwtSignatureHandler<TPrincipal>, JwtSignatureHandler<TPrincipal>>()
|
||||
.AddSingleton(sp => Options.Create(new ClaimDescriptor<TPrincipal>
|
||||
{
|
||||
CreateClaims = claimsMapper,
|
||||
CreateSubject = subjectMapper
|
||||
}));
|
||||
}
|
||||
88
DigitalData.Core.Security/Extensions/Extension.cs
Normal file
88
DigitalData.Core.Security/Extensions/Extension.cs
Normal file
@@ -0,0 +1,88 @@
|
||||
using AutoMapper;
|
||||
using DigitalData.Core.Abstractions.Security.Key;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace DigitalData.Core.Security.Extensions;
|
||||
|
||||
internal static class Extension
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a <see cref="DateTime"/> to a formatted string based on the specified format string.
|
||||
/// <br />
|
||||
/// - If the format contains the symbol “//”, the method divides the numeric value obtained from the left side of the format
|
||||
/// by one minus the numeric value obtained from the right side of the format string and adds one. For instance:
|
||||
/// <br />
|
||||
/// - If the date is 02.03.2024 and the format is "MM//2", it extracts the month (02), subtracts one (3), divides it by 2,
|
||||
/// rounds down the outgoing number (1), adds one to the number (resulting in 2).
|
||||
/// <br />
|
||||
/// - If the format does not contain "//", the method uses the default <see cref="DateTime.ToString"/> format.
|
||||
/// <br />
|
||||
/// </summary>
|
||||
/// <param name="date">The <see cref="DateTime"/> value to be formatted.</param>
|
||||
/// <param name="format">The format string that dictates the formatting of the date. If the format includes the "//" symbol,
|
||||
/// it splits the string at "//" and divides the left-side value by the right-side value. The format string can include standard
|
||||
/// <see cref="DateTime.ToString"/> format patterns.</param>
|
||||
/// <returns>A string representation of the formatted date, or the result of the division operation if "//" is present in the format.</returns>
|
||||
/// <exception cref="ArgumentException">Thrown if the format string is invalid, such as having an incorrect number of parts after "//".</exception>
|
||||
/// <exception cref="DivideByZeroException">Thrown if the right side of the "//" contains a zero, resulting in division by zero.</exception>
|
||||
/// <exception cref="FormatException">Thrown if either the left-side or right-side value of "//" cannot be parsed as an integer.</exception>
|
||||
internal static string ToTag(this DateTime date, string format)
|
||||
{
|
||||
if (format is not null && format.Contains("//"))
|
||||
{
|
||||
var subStrings = format.Split("//");
|
||||
|
||||
if (subStrings.Length != 2)
|
||||
throw new ArgumentException($"Date tag format {format} is invalid. It must contain exactly one '//' separator.", nameof(format));
|
||||
|
||||
var formattedLeft = date.ToString(subStrings[0]);
|
||||
|
||||
if (!int.TryParse(formattedLeft, out var dateValue))
|
||||
throw new FormatException($"The left-side value ({formattedLeft}) of the format could not be parsed to an integer.");
|
||||
|
||||
if (!int.TryParse(subStrings[1], out var divisor))
|
||||
throw new FormatException($"The right-side value ({divisor}) of the format could not be parsed to an integer.");
|
||||
|
||||
if (divisor == 0)
|
||||
throw new DivideByZeroException($"Date tag format {format} includes division by zero, which is not allowed.");
|
||||
|
||||
var result = (dateValue - 1) / divisor + 1;
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
return date.ToString(format);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a <see cref="DateTime"/> to a formatted string based on the specified format string.
|
||||
/// <br />
|
||||
/// - If the format contains the symbol “//”, the method divides the numeric value obtained from the left side of the format
|
||||
/// by one minus the numeric value obtained from the right side of the format string and adds one. For instance:
|
||||
/// <br />
|
||||
/// - If the date is 02.03.2024 and the format is "MM//2", it extracts the month (02), subtracts one (3), divides it by 2,
|
||||
/// rounds down the outgoing number (1), adds one to the number (resulting in 2).
|
||||
/// <br />
|
||||
/// - If the format does not contain "//", the method uses the default <see cref="DateTime.ToString"/> format.
|
||||
/// <br />
|
||||
/// This method provides a way to format the date based on typical or customized rules, including mathematical operations like division.
|
||||
/// </summary>
|
||||
/// <param name="date">The <see cref="DateOnly"/> value to be formatted. It will convert to DateTime to use the method shared with DateTime.</param>
|
||||
/// <param name="format">The format string that dictates the formatting of the date. If the format includes the "//" symbol,
|
||||
/// it splits the string at "//" and divides the left-side value by the right-side value. The format string can include standard
|
||||
/// <see cref="DateTime.ToString"/> format patterns.</param>
|
||||
/// <returns>A string representation of the formatted date, or the result of the division operation if "//" is present in the format.</returns>
|
||||
/// <exception cref="ArgumentException">Thrown if the format string is invalid, such as having an incorrect number of parts after "//".</exception>
|
||||
/// <exception cref="DivideByZeroException">Thrown if the right side of the "//" contains a zero, resulting in division by zero.</exception>
|
||||
/// <exception cref="FormatException">Thrown if either the left-side or right-side value of "//" cannot be parsed as an integer.</exception>
|
||||
internal static string ToTag(this DateOnly date, string format) => date.ToDateTime(new()).ToTag(format);
|
||||
|
||||
/// <summary>
|
||||
/// Maps a <see cref="RSATokenDescriptor"/> to a <see cref="SecurityTokenDescriptor"/>.
|
||||
/// </summary>
|
||||
/// <param name="mapper">The <see cref="IMapper"/> instance used for mapping.</param>
|
||||
/// <param name="description">The <see cref="RSATokenDescriptor"/> instance to be mapped.</param>
|
||||
/// <returns>A <see cref="SecurityTokenDescriptor"/> instance populated with the mapped values.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown if <paramref name="mapper"/> or <paramref name="description"/> is <c>null</c>.</exception>
|
||||
internal static SecurityTokenDescriptor Map(this IMapper mapper, IAsymmetricTokenDescriptor description)
|
||||
=> mapper.Map(description, new SecurityTokenDescriptor());
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
using DigitalData.Core.Abstractions.Security;
|
||||
using DigitalData.Core.Security.Config;
|
||||
using DigitalData.Core.Security.RSAKey;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace DigitalData.Core.Security
|
||||
{
|
||||
public static class Instance
|
||||
{
|
||||
private static readonly Lazy<RSAFactory<RSAFactoryParams>> LazyInstance = new(() => new(Options.Create<RSAFactoryParams>(new())));
|
||||
|
||||
public static IAsymmetricKeyFactory RSAFactory => LazyInstance.Value;
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
using AutoMapper;
|
||||
using DigitalData.Core.Abstractions.Security;
|
||||
using DigitalData.Core.Security.Config;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
|
||||
namespace DigitalData.Core.Security
|
||||
{
|
||||
public class JwtSignatureHandler<TPrincipal> : JwtSecurityTokenHandler, IJwtSignatureHandler<TPrincipal>
|
||||
{
|
||||
private readonly ClaimDescriptor<TPrincipal> _claimDescriptor;
|
||||
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
private readonly ICryptoFactory _cryptoFactory;
|
||||
|
||||
public JwtSignatureHandler(IOptions<ClaimDescriptor<TPrincipal>> claimDescriptorOptions, IMapper mapper, ICryptoFactory cryptoFactory)
|
||||
{
|
||||
_claimDescriptor = claimDescriptorOptions.Value;
|
||||
_mapper = mapper;
|
||||
_cryptoFactory = cryptoFactory;
|
||||
}
|
||||
|
||||
public SecurityToken CreateToken(TPrincipal subject, IAsymmetricTokenDescriptor descriptor)
|
||||
{
|
||||
var sDescriptor = _mapper.Map(descriptor);
|
||||
sDescriptor.Claims = _claimDescriptor.CreateClaims?.Invoke(subject);
|
||||
sDescriptor.Subject = _claimDescriptor.CreateSubject?.Invoke(subject);
|
||||
return CreateToken(sDescriptor);
|
||||
}
|
||||
|
||||
public SecurityToken CreateToken(TPrincipal subject, string issuer, string audience)
|
||||
{
|
||||
var descriptor = _cryptoFactory.TokenDescriptors.Get(issuer: issuer, audience: audience)
|
||||
?? throw new InvalidOperationException($"No or multiple token description found for issuer '{issuer}' and audience '{audience}'.");
|
||||
return CreateToken(subject: subject, descriptor: descriptor);
|
||||
}
|
||||
}
|
||||
}
|
||||
119
DigitalData.Core.Security/RSAKey/Auth/RSATokenDescriptor.cs
Normal file
119
DigitalData.Core.Security/RSAKey/Auth/RSATokenDescriptor.cs
Normal file
@@ -0,0 +1,119 @@
|
||||
using DigitalData.Core.Abstractions.Security.Key;
|
||||
using DigitalData.Core.Security.RSAKey.Base;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace DigitalData.Core.Security.RSAKey.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// Contains some information which used to create a security token. Designed to abstract <see cref="SecurityTokenDescriptor"/>
|
||||
/// </summary>
|
||||
public class RSATokenDescriptor : RSAPrivateKey, IAsymmetricTokenDescriptor
|
||||
{
|
||||
private readonly Lazy<RSATokenValidator> _lazyTokenValidator;
|
||||
|
||||
public IAsymmetricTokenValidator Validator => _lazyTokenValidator.Value;
|
||||
|
||||
public required TimeSpan Lifetime { get; init; }
|
||||
|
||||
#region SecurityTokenDescriptor Map
|
||||
/// <summary>
|
||||
/// Gets or sets the value of the 'audience' claim.
|
||||
/// </summary>
|
||||
public required string Audience { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Defines the compression algorithm that will be used to compress the JWT token payload.
|
||||
/// </summary>
|
||||
public string CompressionAlgorithm { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="EncryptingCredentials"/> used to create a encrypted security token.
|
||||
/// </summary>
|
||||
public EncryptingCredentials EncryptingCredentials { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value of the 'expiration' claim. This value should be in UTC.
|
||||
/// The expiration time is the sum of DateTime.Now and LifeTime.
|
||||
/// </summary>
|
||||
public DateTime? Expires => DateTime.Now.AddTicks(Lifetime.Ticks);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the issuer of this <see cref="SecurityTokenDescriptor"/>.
|
||||
/// </summary>
|
||||
public required string Issuer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the time the security token was issued. This value should be in UTC.
|
||||
/// </summary>
|
||||
public DateTime? IssuedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the notbefore time for the security token. This value should be in UTC.
|
||||
/// </summary>
|
||||
public DateTime? NotBefore { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the token type.
|
||||
/// <remarks> If provided, this will be added as the value for the 'typ' header parameter. In the case of a JWE, this will be added to both the inner (JWS) and the outer token (JWE) header. By default, the value used is 'JWT'.
|
||||
/// If <see cref="AdditionalHeaderClaims"/> also contains 'typ' header claim value, it will override the TokenType provided here.
|
||||
/// This value is used only for JWT tokens and not for SAML/SAML2 tokens</remarks>
|
||||
/// </summary>
|
||||
public string TokenType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="Dictionary{TKey, TValue}"/> which contains any custom header claims that need to be added to the JWT token header.
|
||||
/// The 'alg', 'kid', 'x5t', 'enc', and 'zip' claims are added by default based on the <see cref="SigningCredentials"/>,
|
||||
/// <see cref="EncryptingCredentials"/>, and/or <see cref="CompressionAlgorithm"/> provided and SHOULD NOT be included in this dictionary as this
|
||||
/// will result in an exception being thrown.
|
||||
/// <remarks> These claims are only added to the outer header (in case of a JWE).</remarks>
|
||||
/// </summary>
|
||||
public IDictionary<string, object> AdditionalHeaderClaims { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="Dictionary{TKey, TValue}"/> which contains any custom header claims that need to be added to the inner JWT token header.
|
||||
/// The 'alg', 'kid', 'x5t', 'enc', and 'zip' claims are added by default based on the <see cref="SigningCredentials"/>,
|
||||
/// <see cref="EncryptingCredentials"/>, and/or <see cref="CompressionAlgorithm"/> provided and SHOULD NOT be included in this dictionary as this
|
||||
/// will result in an exception being thrown.
|
||||
/// <remarks>
|
||||
/// For JsonWebTokenHandler, these claims are merged with <see cref="AdditionalHeaderClaims"/> while adding to the inner JWT header.
|
||||
/// </remarks>
|
||||
/// </summary>
|
||||
public IDictionary<string, object> AdditionalInnerHeaderClaims { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="SigningCredentials"/> used to create a security token.
|
||||
/// </summary>
|
||||
public SigningCredentials SigningCredentials => _lazySigningCredentials.Value;
|
||||
#endregion SecurityTokenDescriptor
|
||||
|
||||
private readonly Lazy<RsaSecurityKey> _lazyRsaSecurityKey;
|
||||
|
||||
public SecurityKey SecurityKey => _lazyRsaSecurityKey.Value;
|
||||
|
||||
private readonly Lazy<SigningCredentials> _lazySigningCredentials;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the signature algorithm to be applied to the <see cref="SigningCredentials"/>.
|
||||
/// Default is <see cref="SecurityAlgorithms.RsaSha256"/>.
|
||||
/// </summary>
|
||||
public string SigningAlgorithm { get; init; } = SecurityAlgorithms.RsaSha256;
|
||||
|
||||
/// <summary>
|
||||
/// Optionally specifies the digest algorithm to be applied during the signing process for the <see cref="SigningCredentials"/>.
|
||||
/// If not provided, the default algorithm is used.
|
||||
/// </summary>
|
||||
public string? SigningDigest { get; init; }
|
||||
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
public RSATokenDescriptor()
|
||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
{
|
||||
_lazyTokenValidator = new(CreatePublicKey<RSATokenValidator>);
|
||||
|
||||
_lazyRsaSecurityKey = new(() => new RsaSecurityKey(RSA));
|
||||
|
||||
_lazySigningCredentials = new(() => SigningDigest is null
|
||||
? new(SecurityKey, SigningAlgorithm)
|
||||
: new(SecurityKey, SigningAlgorithm, SigningDigest));
|
||||
}
|
||||
}
|
||||
17
DigitalData.Core.Security/RSAKey/Auth/RSATokenValidator.cs
Normal file
17
DigitalData.Core.Security/RSAKey/Auth/RSATokenValidator.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using DigitalData.Core.Abstractions.Security.Key;
|
||||
using DigitalData.Core.Security.RSAKey.Base;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace DigitalData.Core.Security.RSAKey.Auth;
|
||||
|
||||
public class RSATokenValidator : RSAPublicKey, IAsymmetricTokenValidator
|
||||
{
|
||||
private readonly Lazy<RsaSecurityKey> _lazyRsaSecurityKey;
|
||||
|
||||
public SecurityKey SecurityKey => _lazyRsaSecurityKey.Value;
|
||||
|
||||
public RSATokenValidator()
|
||||
{
|
||||
_lazyRsaSecurityKey = new(() => new RsaSecurityKey(RSA));
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,15 @@
|
||||
using DigitalData.Core.Abstractions.Security;
|
||||
using DigitalData.Core.Abstractions.Security.Key;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace DigitalData.Core.Security.RSAKey
|
||||
namespace DigitalData.Core.Security.RSAKey.Base;
|
||||
|
||||
public class RSAKeyBase : IAsymmetricKey
|
||||
{
|
||||
public class RSAKeyBase : IAsymmetricKey
|
||||
{
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
public virtual string Content { get; init; }
|
||||
public virtual string Content { get; init; }
|
||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
|
||||
public string? Id { get; init; }
|
||||
public string? Id { get; init; }
|
||||
|
||||
protected virtual RSA RSA { get; } = RSA.Create();
|
||||
}
|
||||
protected virtual RSA RSA { get; } = RSA.Create();
|
||||
}
|
||||
54
DigitalData.Core.Security/RSAKey/Base/RSAPrivateKey.cs
Normal file
54
DigitalData.Core.Security/RSAKey/Base/RSAPrivateKey.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using DigitalData.Core.Abstractions.Security.Key;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace DigitalData.Core.Security.RSAKey.Base;
|
||||
|
||||
public class RSAPrivateKey : RSAKeyBase, IAsymmetricPrivateKey, IAsymmetricKey
|
||||
{
|
||||
private string? _pem;
|
||||
|
||||
public override string Content
|
||||
{
|
||||
#pragma warning disable CS8603 // Possible null reference return.
|
||||
get => _pem;
|
||||
#pragma warning restore CS8603 // Possible null reference return.
|
||||
init
|
||||
{
|
||||
_pem = value;
|
||||
Init();
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsPemNull => _pem is null;
|
||||
|
||||
public bool IsEncrypted { get; init; }
|
||||
|
||||
protected TPublicKey CreatePublicKey<TPublicKey>() where TPublicKey : RSAPublicKey, new()
|
||||
=> new() { Content = RSA.ExportRSAPublicKeyPem() };
|
||||
|
||||
private readonly Lazy<RSAPublicKey> _lazyPublicKey;
|
||||
|
||||
public IAsymmetricPublicKey PublicKey => _lazyPublicKey.Value;
|
||||
|
||||
public RSAPrivateKey()
|
||||
{
|
||||
_lazyPublicKey = new(CreatePublicKey<RSAPublicKey>);
|
||||
}
|
||||
|
||||
internal void SetPem(string pem)
|
||||
{
|
||||
_pem = pem;
|
||||
Init();
|
||||
}
|
||||
|
||||
private void Init()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_pem))
|
||||
throw new InvalidOperationException($"The content of RSA private key is null or empty. Id: {Id}.");
|
||||
|
||||
if (IsEncrypted)
|
||||
RSA.ImportFromEncryptedPem(Content, Secrets.PBE_PASSWORD.AsSpan());
|
||||
else
|
||||
RSA.ImportFromPem(Content);
|
||||
}
|
||||
}
|
||||
16
DigitalData.Core.Security/RSAKey/Base/RSAPublicKey.cs
Normal file
16
DigitalData.Core.Security/RSAKey/Base/RSAPublicKey.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using DigitalData.Core.Abstractions.Security.Key;
|
||||
|
||||
namespace DigitalData.Core.Security.RSAKey.Base;
|
||||
|
||||
public class RSAPublicKey : RSAKeyBase, IAsymmetricPublicKey, IAsymmetricKey
|
||||
{
|
||||
public override string Content
|
||||
{
|
||||
get => base.Content;
|
||||
init
|
||||
{
|
||||
base.Content = value;
|
||||
RSA.ImportFromPem(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
33
DigitalData.Core.Security/RSAKey/Crypto/RSADecryptor.cs
Normal file
33
DigitalData.Core.Security/RSAKey/Crypto/RSADecryptor.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using DigitalData.Core.Abstractions.Security.Key;
|
||||
using DigitalData.Core.Security.RSAKey.Base;
|
||||
using System.Reflection;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace DigitalData.Core.Security.RSAKey.Crypto;
|
||||
|
||||
public class RSADecryptor : RSAPrivateKey, IAsymmetricDecryptor
|
||||
{
|
||||
public RSAEncryptionPadding Padding { get; init; } = RSAEncryptionPadding.OaepSHA256;
|
||||
|
||||
// TODO: add as json converter to IConfigurIConfiguration.Config
|
||||
public string PaddingName
|
||||
{
|
||||
get => Padding.ToString();
|
||||
init => Padding = typeof(RSAEncryptionPadding).GetProperty(value, BindingFlags.Public | BindingFlags.Static)?.GetValue(null) as RSAEncryptionPadding ?? throw new ArgumentException($"Padding '{value}' not found.");
|
||||
}
|
||||
|
||||
public byte[] Decrypt(byte[] data) => RSA.Decrypt(data, Padding);
|
||||
|
||||
private readonly Lazy<IAsymmetricEncryptor> _lazyEncryptor;
|
||||
|
||||
public IAsymmetricEncryptor Encryptor => _lazyEncryptor.Value;
|
||||
|
||||
public RSADecryptor()
|
||||
{
|
||||
_lazyEncryptor = new(() => new RSAEncryptor()
|
||||
{
|
||||
Content = RSA.ExportRSAPublicKeyPem(),
|
||||
Padding = Padding
|
||||
});
|
||||
}
|
||||
}
|
||||
20
DigitalData.Core.Security/RSAKey/Crypto/RSAEncryptor.cs
Normal file
20
DigitalData.Core.Security/RSAKey/Crypto/RSAEncryptor.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using DigitalData.Core.Abstractions.Security.Key;
|
||||
using DigitalData.Core.Security.RSAKey.Base;
|
||||
using System.Reflection;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace DigitalData.Core.Security.RSAKey.Crypto;
|
||||
|
||||
public class RSAEncryptor : RSAPublicKey, IAsymmetricEncryptor
|
||||
{
|
||||
public RSAEncryptionPadding Padding { get; init; } = RSAEncryptionPadding.OaepSHA256;
|
||||
|
||||
// TODO: add as json converter to IConfigurIConfiguration.Config
|
||||
public string PaddingName
|
||||
{
|
||||
get => Padding.ToString();
|
||||
init => Padding = typeof(RSAEncryptionPadding).GetProperty(value, BindingFlags.Public | BindingFlags.Static)?.GetValue(null) as RSAEncryptionPadding ?? throw new ArgumentException($"Padding '{value}' not found.");
|
||||
}
|
||||
|
||||
public byte[] Encrypt(byte[] data) => RSA.Encrypt(data, Padding);
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
using DigitalData.Core.Abstractions.Security;
|
||||
using System.Reflection;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace DigitalData.Core.Security.RSAKey
|
||||
{
|
||||
public class RSADecryptor : RSAPrivateKey, IAsymmetricDecryptor
|
||||
{
|
||||
public RSAEncryptionPadding Padding { get; init; } = RSAEncryptionPadding.OaepSHA256;
|
||||
|
||||
// TODO: add as json converter to IConfigurIConfiguration.Config
|
||||
public string PaddingName
|
||||
{
|
||||
get => Padding.ToString();
|
||||
init => Padding = typeof(RSAEncryptionPadding).GetProperty(value, BindingFlags.Public | BindingFlags.Static)?.GetValue(null) as RSAEncryptionPadding ?? throw new ArgumentException($"Padding '{value}' not found.");
|
||||
}
|
||||
|
||||
public byte[] Decrypt(byte[] data) => RSA.Decrypt(data, Padding);
|
||||
|
||||
private readonly Lazy<IAsymmetricEncryptor> _lazyEncryptor;
|
||||
|
||||
public IAsymmetricEncryptor Encryptor => _lazyEncryptor.Value;
|
||||
|
||||
public RSADecryptor()
|
||||
{
|
||||
_lazyEncryptor = new(() => new RSAEncryptor()
|
||||
{
|
||||
Content = RSA.ExportRSAPublicKeyPem(),
|
||||
Padding = Padding
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
using DigitalData.Core.Abstractions.Security;
|
||||
using System.Reflection;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace DigitalData.Core.Security.RSAKey
|
||||
{
|
||||
public class RSAEncryptor : RSAPublicKey, IAsymmetricEncryptor
|
||||
{
|
||||
public RSAEncryptionPadding Padding { get; init; } = RSAEncryptionPadding.OaepSHA256;
|
||||
|
||||
// TODO: add as json converter to IConfigurIConfiguration.Config
|
||||
public string PaddingName
|
||||
{
|
||||
get => Padding.ToString();
|
||||
init => Padding = typeof(RSAEncryptionPadding).GetProperty(value, BindingFlags.Public | BindingFlags.Static)?.GetValue(null) as RSAEncryptionPadding ?? throw new ArgumentException($"Padding '{value}' not found.");
|
||||
}
|
||||
|
||||
public byte[] Encrypt(byte[] data) => RSA.Encrypt(data, Padding);
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
using DigitalData.Core.Abstractions.Security;
|
||||
using DigitalData.Core.Security.Config;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace DigitalData.Core.Security.RSAKey
|
||||
{
|
||||
public class RSAFactory<TRSAFactoryParams> : IAsymmetricKeyFactory where TRSAFactoryParams : RSAFactoryParams
|
||||
{
|
||||
protected readonly TRSAFactoryParams _params;
|
||||
|
||||
public RSAFactory(IOptions<TRSAFactoryParams> options)
|
||||
{
|
||||
options.Value.Init();
|
||||
_params = options.Value;
|
||||
}
|
||||
|
||||
public string CreatePrivateKeyPem(int? keySizeInBits = null, bool encrypt = false) => encrypt
|
||||
? CreateEncryptedPrivateKeyPem(keySizeInBits: keySizeInBits)
|
||||
: RSA.Create(keySizeInBits ?? _params.KeySizeInBits).ExportRSAPrivateKeyPem();
|
||||
|
||||
public string CreateEncryptedPrivateKeyPem(
|
||||
PbeEncryptionAlgorithm? pbeEncryptionAlgorithm = null,
|
||||
HashAlgorithmName? hashAlgorithmName = null,
|
||||
int? iterationCount = null,
|
||||
int? keySizeInBits = null,
|
||||
string? password = null)
|
||||
{
|
||||
password ??= _params.PbePassword;
|
||||
|
||||
var pbeParameters = pbeEncryptionAlgorithm is null && hashAlgorithmName is null && iterationCount is null
|
||||
? new PbeParameters(
|
||||
pbeEncryptionAlgorithm ?? _params.PbeEncryptionAlgorithm,
|
||||
hashAlgorithmName ?? _params.PbeHashAlgorithm,
|
||||
iterationCount ?? _params.PbeIterationCount)
|
||||
: _params.PbeParameters;
|
||||
|
||||
var encryptedPrivateKey = RSA.Create(keySizeInBits ?? _params.KeySizeInBits).ExportEncryptedPkcs8PrivateKey(password.AsSpan(), pbeParameters);
|
||||
|
||||
var pemChars = PemEncoding.Write(_params.EncryptedPrivateKeyPemLabel, encryptedPrivateKey);
|
||||
|
||||
return new string(pemChars);
|
||||
}
|
||||
|
||||
public string CreateEncryptedPrivateKeyPem(
|
||||
PbeParameters pbeParameters,
|
||||
int? keySizeInBits = null,
|
||||
string? password = null)
|
||||
{
|
||||
password ??= _params.PbePassword;
|
||||
|
||||
var encryptedPrivateKey = RSA.Create(keySizeInBits ?? _params.KeySizeInBits).ExportEncryptedPkcs8PrivateKey(password.AsSpan(), pbeParameters);
|
||||
|
||||
var pemChars = PemEncoding.Write(_params.EncryptedPrivateKeyPemLabel, encryptedPrivateKey);
|
||||
|
||||
return new string(pemChars);
|
||||
}
|
||||
|
||||
public IAsymmetricDecryptor CreateDecryptor(string pem, string? issuer = null, string? audience = null, bool encrypt = false, RSAEncryptionPadding? padding = null) => new RSADecryptor()
|
||||
{
|
||||
Content = pem,
|
||||
IsEncrypted = encrypt,
|
||||
Padding = padding ?? RSAEncryptionPadding.OaepSHA256
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
using DigitalData.Core.Abstractions.Security;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace DigitalData.Core.Security.RSAKey
|
||||
{
|
||||
public class RSAPrivateKey : RSAKeyBase, IAsymmetricPrivateKey, IAsymmetricKey
|
||||
{
|
||||
private string? _pem;
|
||||
|
||||
public override string Content
|
||||
{
|
||||
#pragma warning disable CS8603 // Possible null reference return.
|
||||
get => _pem;
|
||||
#pragma warning restore CS8603 // Possible null reference return.
|
||||
init
|
||||
{
|
||||
_pem = value;
|
||||
Init();
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsPemNull => _pem is null;
|
||||
|
||||
public bool IsEncrypted { get; init; }
|
||||
|
||||
protected TPublicKey CreatePublicKey<TPublicKey>() where TPublicKey : RSAPublicKey, new()
|
||||
=> new() { Content = RSA.ExportRSAPublicKeyPem() };
|
||||
|
||||
private readonly Lazy<RSAPublicKey> _lazyPublicKey;
|
||||
|
||||
public IAsymmetricPublicKey PublicKey => _lazyPublicKey.Value;
|
||||
|
||||
public RSAPrivateKey()
|
||||
{
|
||||
_lazyPublicKey = new(CreatePublicKey<RSAPublicKey>);
|
||||
}
|
||||
|
||||
internal void SetPem(string pem)
|
||||
{
|
||||
_pem = pem;
|
||||
Init();
|
||||
}
|
||||
|
||||
private void Init()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_pem))
|
||||
throw new InvalidOperationException ($"The content of RSA private key is null or empty. Id: {Id}.");
|
||||
|
||||
if (IsEncrypted)
|
||||
RSA.ImportFromEncryptedPem(Content, Secrets.PBE_PASSWORD.AsSpan());
|
||||
else
|
||||
RSA.ImportFromPem(Content);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
using DigitalData.Core.Abstractions.Security;
|
||||
|
||||
namespace DigitalData.Core.Security.RSAKey
|
||||
{
|
||||
public class RSAPublicKey : RSAKeyBase, IAsymmetricPublicKey, IAsymmetricKey
|
||||
{
|
||||
public override string Content
|
||||
{
|
||||
get => base.Content;
|
||||
init
|
||||
{
|
||||
base.Content = value;
|
||||
RSA.ImportFromPem(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
using DigitalData.Core.Abstractions.Security;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace DigitalData.Core.Security.RSAKey
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains some information which used to create a security token. Designed to abstract <see cref="SecurityTokenDescriptor"/>
|
||||
/// </summary>
|
||||
public class RSATokenDescriptor : RSAPrivateKey, IAsymmetricTokenDescriptor
|
||||
{
|
||||
private readonly Lazy<RSATokenValidator> _lazyTokenValidator;
|
||||
|
||||
public IAsymmetricTokenValidator Validator => _lazyTokenValidator.Value;
|
||||
|
||||
public required TimeSpan Lifetime { get; init; }
|
||||
|
||||
#region SecurityTokenDescriptor Map
|
||||
/// <summary>
|
||||
/// Gets or sets the value of the 'audience' claim.
|
||||
/// </summary>
|
||||
public required string Audience { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Defines the compression algorithm that will be used to compress the JWT token payload.
|
||||
/// </summary>
|
||||
public string CompressionAlgorithm { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="EncryptingCredentials"/> used to create a encrypted security token.
|
||||
/// </summary>
|
||||
public EncryptingCredentials EncryptingCredentials { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value of the 'expiration' claim. This value should be in UTC.
|
||||
/// The expiration time is the sum of DateTime.Now and LifeTime.
|
||||
/// </summary>
|
||||
public DateTime? Expires => DateTime.Now.AddTicks(Lifetime.Ticks);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the issuer of this <see cref="SecurityTokenDescriptor"/>.
|
||||
/// </summary>
|
||||
public required string Issuer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the time the security token was issued. This value should be in UTC.
|
||||
/// </summary>
|
||||
public DateTime? IssuedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the notbefore time for the security token. This value should be in UTC.
|
||||
/// </summary>
|
||||
public DateTime? NotBefore { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the token type.
|
||||
/// <remarks> If provided, this will be added as the value for the 'typ' header parameter. In the case of a JWE, this will be added to both the inner (JWS) and the outer token (JWE) header. By default, the value used is 'JWT'.
|
||||
/// If <see cref="AdditionalHeaderClaims"/> also contains 'typ' header claim value, it will override the TokenType provided here.
|
||||
/// This value is used only for JWT tokens and not for SAML/SAML2 tokens</remarks>
|
||||
/// </summary>
|
||||
public string TokenType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="Dictionary{TKey, TValue}"/> which contains any custom header claims that need to be added to the JWT token header.
|
||||
/// The 'alg', 'kid', 'x5t', 'enc', and 'zip' claims are added by default based on the <see cref="SigningCredentials"/>,
|
||||
/// <see cref="EncryptingCredentials"/>, and/or <see cref="CompressionAlgorithm"/> provided and SHOULD NOT be included in this dictionary as this
|
||||
/// will result in an exception being thrown.
|
||||
/// <remarks> These claims are only added to the outer header (in case of a JWE).</remarks>
|
||||
/// </summary>
|
||||
public IDictionary<string, object> AdditionalHeaderClaims { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="Dictionary{TKey, TValue}"/> which contains any custom header claims that need to be added to the inner JWT token header.
|
||||
/// The 'alg', 'kid', 'x5t', 'enc', and 'zip' claims are added by default based on the <see cref="SigningCredentials"/>,
|
||||
/// <see cref="EncryptingCredentials"/>, and/or <see cref="CompressionAlgorithm"/> provided and SHOULD NOT be included in this dictionary as this
|
||||
/// will result in an exception being thrown.
|
||||
/// <remarks>
|
||||
/// For JsonWebTokenHandler, these claims are merged with <see cref="AdditionalHeaderClaims"/> while adding to the inner JWT header.
|
||||
/// </remarks>
|
||||
/// </summary>
|
||||
public IDictionary<string, object> AdditionalInnerHeaderClaims { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="SigningCredentials"/> used to create a security token.
|
||||
/// </summary>
|
||||
public SigningCredentials SigningCredentials => _lazySigningCredentials.Value;
|
||||
#endregion SecurityTokenDescriptor
|
||||
|
||||
private readonly Lazy<RsaSecurityKey> _lazyRsaSecurityKey;
|
||||
|
||||
public SecurityKey SecurityKey => _lazyRsaSecurityKey.Value;
|
||||
|
||||
private readonly Lazy<SigningCredentials> _lazySigningCredentials;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the signature algorithm to be applied to the <see cref="SigningCredentials"/>.
|
||||
/// Default is <see cref="SecurityAlgorithms.RsaSha256"/>.
|
||||
/// </summary>
|
||||
public string SigningAlgorithm { get; init; } = SecurityAlgorithms.RsaSha256;
|
||||
|
||||
/// <summary>
|
||||
/// Optionally specifies the digest algorithm to be applied during the signing process for the <see cref="SigningCredentials"/>.
|
||||
/// If not provided, the default algorithm is used.
|
||||
/// </summary>
|
||||
public string? SigningDigest { get; init; }
|
||||
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
public RSATokenDescriptor()
|
||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
{
|
||||
_lazyTokenValidator = new(CreatePublicKey<RSATokenValidator>);
|
||||
|
||||
_lazyRsaSecurityKey = new(() => new RsaSecurityKey(RSA));
|
||||
|
||||
_lazySigningCredentials = new(() => SigningDigest is null
|
||||
? new(SecurityKey, SigningAlgorithm)
|
||||
: new(SecurityKey, SigningAlgorithm, SigningDigest));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
using DigitalData.Core.Abstractions.Security;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace DigitalData.Core.Security.RSAKey
|
||||
{
|
||||
public class RSATokenValidator : RSAPublicKey, IAsymmetricTokenValidator
|
||||
{
|
||||
private readonly Lazy<RsaSecurityKey> _lazyRsaSecurityKey;
|
||||
|
||||
public SecurityKey SecurityKey => _lazyRsaSecurityKey.Value;
|
||||
|
||||
public RSATokenValidator()
|
||||
{
|
||||
_lazyRsaSecurityKey = new(() => new RsaSecurityKey(RSA));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace DigitalData.Core.Security
|
||||
namespace DigitalData.Core.Security;
|
||||
|
||||
public static class Secrets
|
||||
{
|
||||
public static class Secrets
|
||||
{
|
||||
public static readonly DateTime CreationDate = new (2024, 11, 19);
|
||||
public static readonly DateTime CreationDate = new (2024, 11, 19);
|
||||
|
||||
public static readonly Version Version = new (1, 0);
|
||||
public static readonly Version Version = new (1, 0);
|
||||
|
||||
[JsonIgnore]
|
||||
internal static readonly string PBE_PASSWORD = "9mk@i/$QY&Mw@_--dI^ahlXpNKEtv_U-,V-46b19_-Z6-U_*89_n1_-5-r-_+_$_IY_mYQl-";
|
||||
}
|
||||
[JsonIgnore]
|
||||
internal static readonly string PBE_PASSWORD = "9mk@i/$QY&Mw@_--dI^ahlXpNKEtv_U-,V-46b19_-Z6-U_*89_n1_-5-r-_+_$_IY_mYQl-";
|
||||
}
|
||||
42
DigitalData.Core.Security/Services/JwtSignatureHandler.cs
Normal file
42
DigitalData.Core.Security/Services/JwtSignatureHandler.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using AutoMapper;
|
||||
using DigitalData.Core.Abstractions.Security.Extensions;
|
||||
using DigitalData.Core.Abstractions.Security.Key;
|
||||
using DigitalData.Core.Abstractions.Security.Services;
|
||||
using DigitalData.Core.Security.Config;
|
||||
using DigitalData.Core.Security.Extensions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
|
||||
namespace DigitalData.Core.Security.Services;
|
||||
|
||||
public class JwtSignatureHandler<TPrincipal> : JwtSecurityTokenHandler, IJwtSignatureHandler<TPrincipal>
|
||||
{
|
||||
private readonly ClaimDescriptor<TPrincipal> _claimDescriptor;
|
||||
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
private readonly IAsymmetricKeyPool _cryptoFactory;
|
||||
|
||||
public JwtSignatureHandler(IOptions<ClaimDescriptor<TPrincipal>> claimDescriptorOptions, IMapper mapper, IAsymmetricKeyPool cryptoFactory)
|
||||
{
|
||||
_claimDescriptor = claimDescriptorOptions.Value;
|
||||
_mapper = mapper;
|
||||
_cryptoFactory = cryptoFactory;
|
||||
}
|
||||
|
||||
public SecurityToken CreateToken(TPrincipal subject, IAsymmetricTokenDescriptor descriptor)
|
||||
{
|
||||
var sDescriptor = _mapper.Map(descriptor);
|
||||
sDescriptor.Claims = _claimDescriptor.CreateClaims?.Invoke(subject);
|
||||
sDescriptor.Subject = _claimDescriptor.CreateSubject?.Invoke(subject);
|
||||
return CreateToken(sDescriptor);
|
||||
}
|
||||
|
||||
public SecurityToken CreateToken(TPrincipal subject, string issuer, string audience)
|
||||
{
|
||||
var descriptor = _cryptoFactory.TokenDescriptors.Get(issuer: issuer, audience: audience)
|
||||
?? throw new InvalidOperationException($"No or multiple token description found for issuer '{issuer}' and audience '{audience}'.");
|
||||
return CreateToken(subject: subject, descriptor: descriptor);
|
||||
}
|
||||
}
|
||||
88
DigitalData.Core.Security/Services/PemFileInitalizer.cs
Normal file
88
DigitalData.Core.Security/Services/PemFileInitalizer.cs
Normal file
@@ -0,0 +1,88 @@
|
||||
using DigitalData.Core.Security.Config;
|
||||
using DigitalData.Core.Security.Extensions;
|
||||
using DigitalData.Core.Security.RSAKey.Auth;
|
||||
using DigitalData.Core.Security.RSAKey.Base;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace DigitalData.Core.Security.Services;
|
||||
|
||||
public class PemFileInitalizer : BackgroundService
|
||||
{
|
||||
private readonly RSAParams _factoryParams;
|
||||
|
||||
private readonly ILogger<PemFileInitalizer> _logger;
|
||||
|
||||
public PemFileInitalizer(IOptions<RSAParams> factoryParamsOptions, ILogger<PemFileInitalizer> logger)
|
||||
{
|
||||
_factoryParams = factoryParamsOptions.Value;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Pem file initalizer launched.");
|
||||
await InitPemFiles(stoppingToken);
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Pem files cannot be initialized.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task InitPemFiles(CancellationToken stoppingToken = default)
|
||||
{
|
||||
_logger.LogInformation("Initializing pem files. PemDirectory: {dir}", _factoryParams.PemDirectory);
|
||||
// Create root folder if it does not exist
|
||||
if (!Directory.Exists(_factoryParams.PemDirectory))
|
||||
Directory.CreateDirectory(_factoryParams.PemDirectory);
|
||||
|
||||
var privateKeys = new List<RSAPrivateKey>();
|
||||
privateKeys.AddRange(_factoryParams.Decryptors);
|
||||
privateKeys.AddRange(_factoryParams.TokenDescriptors);
|
||||
if (_factoryParams.VaultDecryptor is not null)
|
||||
privateKeys.Add(_factoryParams.VaultDecryptor);
|
||||
|
||||
foreach (var privateKey in privateKeys)
|
||||
{
|
||||
// set default path
|
||||
if (privateKey.IsPemNull)
|
||||
{
|
||||
// file name
|
||||
var file_name_params = new List<object>();
|
||||
|
||||
if (privateKey.Id is not null)
|
||||
file_name_params.Add(privateKey.Id);
|
||||
else if (privateKey is RSATokenDescriptor descriptor)
|
||||
file_name_params.Add(descriptor.Issuer);
|
||||
|
||||
file_name_params.Add(_factoryParams.KeySizeInBits);
|
||||
file_name_params.Add(DateTime.Now.ToTag(_factoryParams.DateTagFormat));
|
||||
|
||||
if (privateKey.IsEncrypted)
|
||||
file_name_params.Add(Secrets.Version);
|
||||
|
||||
var file_name = $"{string.Join(_factoryParams.FileNameSeparator, file_name_params)}.{_factoryParams.FileExtension}";
|
||||
|
||||
var path = Path.Combine(_factoryParams.PemDirectory, file_name);
|
||||
|
||||
if (File.Exists(path))
|
||||
privateKey.SetPem(File.ReadAllText(path));
|
||||
else
|
||||
{
|
||||
var pem = privateKey.IsEncrypted
|
||||
? RSAFactory.Static.CreateEncryptedPrivateKeyPem(pbeParameters: _factoryParams.PbeParameters, keySizeInBits: _factoryParams.KeySizeInBits, password: Secrets.PBE_PASSWORD)
|
||||
: RSAFactory.Static.CreatePrivateKeyPem(keySizeInBits: _factoryParams.KeySizeInBits);
|
||||
|
||||
privateKey.SetPem(pem);
|
||||
|
||||
// Save file in background
|
||||
await File.WriteAllTextAsync(path: path, pem, stoppingToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
58
DigitalData.Core.Security/Services/RSAFactory.cs
Normal file
58
DigitalData.Core.Security/Services/RSAFactory.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using DigitalData.Core.Abstractions.Security.Key;
|
||||
using DigitalData.Core.Abstractions.Security.Services;
|
||||
using DigitalData.Core.Security.Config;
|
||||
using DigitalData.Core.Security.RSAKey.Crypto;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace DigitalData.Core.Security.Services;
|
||||
|
||||
public class RSAFactory : IAsymmetricKeyFactory
|
||||
{
|
||||
public static readonly RSAFactory Static = new();
|
||||
|
||||
public string CreatePrivateKeyPem(int? keySizeInBits = null, bool encrypt = false) => encrypt
|
||||
? CreateEncryptedPrivateKeyPem(keySizeInBits: keySizeInBits)
|
||||
: RSA.Create(keySizeInBits ?? RSAParams.Default.KeySizeInBits).ExportRSAPrivateKeyPem();
|
||||
|
||||
public string CreateEncryptedPrivateKeyPem(
|
||||
PbeEncryptionAlgorithm? pbeEncryptionAlgorithm = null,
|
||||
HashAlgorithmName? hashAlgorithmName = null,
|
||||
int? iterationCount = null,
|
||||
int? keySizeInBits = null,
|
||||
string? password = null)
|
||||
{
|
||||
password ??= RSAParams.Default.PbePassword;
|
||||
|
||||
var pbeParameters = new PbeParameters(
|
||||
pbeEncryptionAlgorithm ?? RSAParams.Default.PbeEncryptionAlgorithm,
|
||||
hashAlgorithmName ?? RSAParams.Default.PbeHashAlgorithm,
|
||||
iterationCount ?? RSAParams.Default.PbeIterationCount);
|
||||
|
||||
var encryptedPrivateKey = RSA.Create(keySizeInBits ?? RSAParams.Default.KeySizeInBits).ExportEncryptedPkcs8PrivateKey(password.AsSpan(), pbeParameters);
|
||||
|
||||
var pemChars = PemEncoding.Write(RSAParams.Default.EncryptedPrivateKeyPemLabel, encryptedPrivateKey);
|
||||
|
||||
return new string(pemChars);
|
||||
}
|
||||
|
||||
public string CreateEncryptedPrivateKeyPem(
|
||||
PbeParameters pbeParameters,
|
||||
int? keySizeInBits = null,
|
||||
string? password = null)
|
||||
{
|
||||
password ??= RSAParams.Default.PbePassword;
|
||||
|
||||
var encryptedPrivateKey = RSA.Create(keySizeInBits ?? RSAParams.Default.KeySizeInBits).ExportEncryptedPkcs8PrivateKey(password.AsSpan(), pbeParameters);
|
||||
|
||||
var pemChars = PemEncoding.Write(RSAParams.Default.EncryptedPrivateKeyPemLabel, encryptedPrivateKey);
|
||||
|
||||
return new string(pemChars);
|
||||
}
|
||||
|
||||
public IAsymmetricDecryptor CreateDecryptor(string pem, string? issuer = null, string? audience = null, bool encrypt = false, RSAEncryptionPadding? padding = null) => new RSADecryptor()
|
||||
{
|
||||
Content = pem,
|
||||
IsEncrypted = encrypt,
|
||||
Padding = padding ?? RSAEncryptionPadding.OaepSHA256
|
||||
};
|
||||
}
|
||||
41
DigitalData.Core.Security/Services/RSAPool.cs
Normal file
41
DigitalData.Core.Security/Services/RSAPool.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using DigitalData.Core.Abstractions.Security.Key;
|
||||
using DigitalData.Core.Abstractions.Security.Services;
|
||||
using DigitalData.Core.Security.Config;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace DigitalData.Core.Security.Services;
|
||||
|
||||
public class RSAPool : RSAFactory, IAsymmetricKeyPool, IAsymmetricKeyFactory
|
||||
{
|
||||
private readonly RSAParams _params;
|
||||
|
||||
public IEnumerable<IAsymmetricDecryptor> Decryptors { get; }
|
||||
|
||||
/// <summary>
|
||||
/// It is a separate decryptor for permanently stored encrypted data. It is assigned to the first Default decryptor by default.
|
||||
/// </summary>
|
||||
public IAsymmetricDecryptor VaultDecryptor { get; }
|
||||
|
||||
public IEnumerable<IAsymmetricTokenDescriptor> TokenDescriptors { get; init; } = new List<IAsymmetricTokenDescriptor>();
|
||||
|
||||
public RSAPool(IOptions<RSAParams> cryptoFactoryParamsOptions, ILogger<RSAPool>? logger = null)
|
||||
{
|
||||
_params = cryptoFactoryParamsOptions.Value;
|
||||
|
||||
logger?.LogInformation("Core.Secrets version: {Version}, Created on: {CreationDate}.", Secrets.Version, Secrets.CreationDate.ToString("dd.MM.yyyy"));
|
||||
|
||||
if (!_params.Decryptors.Any())
|
||||
throw new InvalidOperationException(
|
||||
"Any decryptor is not found. Ensure that at least one decryptor is configured in the provided parameters. " +
|
||||
"This issue typically arises if the configuration for decryptors is incomplete or missing. " +
|
||||
"Check the 'Decryptors' collection in the configuration and verify that it contains valid entries."
|
||||
);
|
||||
|
||||
Decryptors = _params.Decryptors;
|
||||
|
||||
TokenDescriptors = _params.TokenDescriptors;
|
||||
|
||||
VaultDecryptor = _params.VaultDecryptor ?? Decryptors.First();
|
||||
}
|
||||
}
|
||||
@@ -13,9 +13,9 @@ namespace DigitalData.Core.Tests.Client
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_serviceProvider = new ServiceCollection()
|
||||
.AddHttpClientService("https://jsonplaceholder.typicode.com", "todos")
|
||||
.BuildServiceProvider();
|
||||
//_serviceProvider = new ServiceCollection()
|
||||
// .AddHttpClientService("https://jsonplaceholder.typicode.com", "todos")
|
||||
// .BuildServiceProvider();
|
||||
|
||||
_service = _serviceProvider.GetRequiredService<IBaseHttpClientService>();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net7.0;net8.0</TargetFrameworks>
|
||||
<TargetFrameworks>net7.0;net8.0;net9.0</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Bogus" Version="35.6.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Localization.Abstractions" Version="7.0.16" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
|
||||
<PackageReference Include="Moq" Version="4.20.70" />
|
||||
@@ -25,8 +26,24 @@
|
||||
<ProjectReference Include="..\DigitalData.Core.Application\DigitalData.Core.Application.csproj" />
|
||||
<ProjectReference Include="..\DigitalData.Core.Client\DigitalData.Core.Client.csproj" />
|
||||
<ProjectReference Include="..\DigitalData.Core.DTO\DigitalData.Core.DTO.csproj" />
|
||||
<ProjectReference Include="..\DigitalData.Core.Infrastructure.AutoMapper\DigitalData.Core.Infrastructure.AutoMapper.csproj" />
|
||||
<ProjectReference Include="..\DigitalData.Core.Infrastructure\DigitalData.Core.Infrastructure.csproj" />
|
||||
<ProjectReference Include="..\DigitalData.Core.Security\DigitalData.Core.Security.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net7.0'">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.20" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="7.0.20" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.15" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.15" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.4" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
113
DigitalData.Core.Tests/Infrastructure/DbRepositoryTests.cs
Normal file
113
DigitalData.Core.Tests/Infrastructure/DbRepositoryTests.cs
Normal file
@@ -0,0 +1,113 @@
|
||||
namespace DigitalData.Core.Tests.Infrastructure;
|
||||
|
||||
using DigitalData.Core.Infrastructure;
|
||||
using DigitalData.Core.Tests.Mock;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using System.Reflection;
|
||||
using DigitalData.Core.Abstractions.Infrastructure;
|
||||
using DigitalData.Core.Infrastructure.AutoMapper;
|
||||
|
||||
public class DbRepositoryTests
|
||||
{
|
||||
private IHost _host;
|
||||
|
||||
private IRepository<User> _userRepo;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
var builder = Host.CreateApplicationBuilder();
|
||||
|
||||
builder.Services.AddDbContext<MockDbContext>(opt => opt.UseInMemoryDatabase("MockDB"));
|
||||
|
||||
builder.Services.AddDbRepository<MockDbContext, User>(context => context.Users).UseAutoMapper(typeof(UserCreateDto), typeof(UserReadDto), typeof(UserBase));
|
||||
|
||||
builder.Services.AddAutoMapper(Assembly.GetExecutingAssembly());
|
||||
|
||||
_host = builder.Build();
|
||||
|
||||
_userRepo = _host.Services.GetRequiredService<IRepository<User>>();
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
if (_host is IDisposable disposableHost)
|
||||
disposableHost.Dispose();
|
||||
}
|
||||
|
||||
[TestCase(true, TestName = "WhenGivenMultipleUsers")]
|
||||
[TestCase(false, TestName = "WhenGivenSingleUser")]
|
||||
public void CreateAsync_ShouldNotThrow(bool multiple)
|
||||
{
|
||||
// Arrange
|
||||
var faker = Fake.CreateUserFaker();
|
||||
|
||||
// Act & Assert
|
||||
if (multiple)
|
||||
Assert.DoesNotThrowAsync(async () => await _userRepo.CreateAsync(faker.Generate(Random.Shared.Next(1, 10))));
|
||||
else
|
||||
Assert.DoesNotThrowAsync(async () => await _userRepo.CreateAsync(faker.Generate()));
|
||||
}
|
||||
|
||||
[TestCase(true, TestName = "WhenDtoUsed")]
|
||||
[TestCase(false, TestName = "WhenEntityUsed")]
|
||||
public async Task ReadAsync_ShouldReturnCreated(bool useDto)
|
||||
{
|
||||
// Act
|
||||
var createdUser = useDto
|
||||
? await _userRepo.CreateAsync(Fake.UserCreateDto)
|
||||
: await _userRepo.CreateAsync(Fake.User);
|
||||
|
||||
var readUser = await _userRepo.ReadFirstOrDefaultAsync(u => u.Id == createdUser.Id);
|
||||
|
||||
// Assert
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(readUser, Is.Not.Null);
|
||||
Assert.That(readUser, Is.EqualTo(createdUser));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ReadAsync_ShouldReturnUpdated()
|
||||
{
|
||||
// Arrange
|
||||
var createdUser = await _userRepo.CreateAsync(Fake.UserCreateDto);
|
||||
var userUpdateDto = new UserUpdateDto() { Age = 10, Email = "Bar", FirstName = "Foo" };
|
||||
|
||||
// Act
|
||||
await _userRepo.UpdateAsync(userUpdateDto, u => u.Id == createdUser!.Id);
|
||||
var upToDateUser = await _userRepo.ReadFirstOrDefaultAsync(u => u.Id == createdUser!.Id);
|
||||
|
||||
// Assert
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(upToDateUser, Is.Not.Null);
|
||||
Assert.That(upToDateUser?.FirstName, Is.EqualTo(userUpdateDto.FirstName));
|
||||
Assert.That(upToDateUser?.Email, Is.EqualTo(userUpdateDto.Email));
|
||||
Assert.That(upToDateUser?.Age, Is.EqualTo(userUpdateDto.Age));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ReadAsync_ShouldNotReturnDeleted()
|
||||
{
|
||||
// Arrange
|
||||
var createdUser = await _userRepo.CreateAsync(Fake.UserCreateDto);
|
||||
var readUser = await _userRepo.ReadFirstOrDefaultAsync(u => u.Id == createdUser.Id);
|
||||
|
||||
// Act
|
||||
await _userRepo.DeleteAsync(u => u.Id == createdUser.Id);
|
||||
var deletedUser = await _userRepo.ReadFirstOrDefaultAsync(u => u.Id == createdUser.Id);
|
||||
|
||||
// Assert
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(readUser, Is.Not.Null);
|
||||
Assert.That(deletedUser, Is.Null);
|
||||
});
|
||||
}
|
||||
}
|
||||
28
DigitalData.Core.Tests/Mock/Fake.cs
Normal file
28
DigitalData.Core.Tests/Mock/Fake.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using Bogus;
|
||||
|
||||
namespace DigitalData.Core.Tests.Mock;
|
||||
|
||||
public static class Fake
|
||||
{
|
||||
public static Faker<User> CreateUserFaker(string? firstName = null, string? email = null, int? age = null) => new Faker<User>()
|
||||
.RuleFor(u => u.FirstName, f => firstName ?? f.Name.FirstName())
|
||||
.RuleFor(u => u.Email, f => email ?? f.Internet.Email())
|
||||
.RuleFor(u => u.Age, f => age ?? f.Random.Int(18, 99));
|
||||
|
||||
private static readonly Faker<User> UserFaker = CreateUserFaker();
|
||||
|
||||
public static User User => UserFaker.Generate();
|
||||
|
||||
public static List<User> Users => UserFaker.Generate(Random.Shared.Next(1, 10));
|
||||
|
||||
public static Faker<UserCreateDto> CreateUserCreateDtoFaker(string? firstName = null, string? email = null, int? age = null) => new Faker<UserCreateDto>()
|
||||
.RuleFor(u => u.FirstName, f => firstName ?? f.Name.FirstName())
|
||||
.RuleFor(u => u.Email, f => email ?? f.Internet.Email())
|
||||
.RuleFor(u => u.Age, f => age ?? f.Random.Int(18, 99));
|
||||
|
||||
private static readonly Faker<UserCreateDto> UserCreateDtoFaker = CreateUserCreateDtoFaker();
|
||||
|
||||
public static UserCreateDto UserCreateDto => UserCreateDtoFaker.Generate();
|
||||
|
||||
public static List<UserCreateDto> UserCreateDtos => UserCreateDtoFaker.Generate(Random.Shared.Next(1, 10));
|
||||
}
|
||||
16
DigitalData.Core.Tests/Mock/MappingProfile.cs
Normal file
16
DigitalData.Core.Tests/Mock/MappingProfile.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using AutoMapper;
|
||||
|
||||
namespace DigitalData.Core.Tests.Mock;
|
||||
|
||||
public class MappingProfile : Profile
|
||||
{
|
||||
public MappingProfile()
|
||||
{
|
||||
// DTO ---> Entity
|
||||
CreateMap<UserCreateDto, User>();
|
||||
CreateMap<UserUpdateDto, User>();
|
||||
|
||||
// Entity ---> DTO
|
||||
CreateMap<User, UserReadDto>();
|
||||
}
|
||||
}
|
||||
12
DigitalData.Core.Tests/Mock/MockDbContext.cs
Normal file
12
DigitalData.Core.Tests/Mock/MockDbContext.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DigitalData.Core.Tests.Mock;
|
||||
|
||||
public class MockDbContext : DbContext
|
||||
{
|
||||
public DbSet<User> Users { get; set; }
|
||||
|
||||
public MockDbContext(DbContextOptions options) : base(options)
|
||||
{
|
||||
}
|
||||
}
|
||||
12
DigitalData.Core.Tests/Mock/User.cs
Normal file
12
DigitalData.Core.Tests/Mock/User.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace DigitalData.Core.Tests.Mock;
|
||||
|
||||
public class User : UserBase
|
||||
{
|
||||
public required int Id { get; init; }
|
||||
|
||||
public override int GetHashCode() => HashCode.Combine(Id, FirstName, Email, Age);
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
=> (obj is User user && user.GetHashCode() == GetHashCode())
|
||||
|| (obj is UserBase userBase && userBase.GetHashCode() == base.GetHashCode());
|
||||
}
|
||||
14
DigitalData.Core.Tests/Mock/UserBase.cs
Normal file
14
DigitalData.Core.Tests/Mock/UserBase.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace DigitalData.Core.Tests.Mock;
|
||||
|
||||
public class UserBase
|
||||
{
|
||||
public required string FirstName { get; set; }
|
||||
|
||||
public required string Email { get; set; }
|
||||
|
||||
public required int Age { get; set; }
|
||||
|
||||
public override int GetHashCode() => HashCode.Combine(FirstName, Email, Age);
|
||||
|
||||
public override bool Equals(object? obj) => obj is UserBase user && user.GetHashCode() == GetHashCode();
|
||||
}
|
||||
5
DigitalData.Core.Tests/Mock/UserCreateDto.cs
Normal file
5
DigitalData.Core.Tests/Mock/UserCreateDto.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
namespace DigitalData.Core.Tests.Mock;
|
||||
|
||||
public class UserCreateDto : UserBase
|
||||
{
|
||||
}
|
||||
5
DigitalData.Core.Tests/Mock/UserReadDto.cs
Normal file
5
DigitalData.Core.Tests/Mock/UserReadDto.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
namespace DigitalData.Core.Tests.Mock;
|
||||
|
||||
public class UserReadDto : UserBase
|
||||
{
|
||||
}
|
||||
5
DigitalData.Core.Tests/Mock/UserUpdateDto.cs
Normal file
5
DigitalData.Core.Tests/Mock/UserUpdateDto.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
namespace DigitalData.Core.Tests.Mock;
|
||||
|
||||
public class UserUpdateDto : UserBase
|
||||
{
|
||||
}
|
||||
@@ -27,6 +27,18 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigitalData.Core.Terminal",
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigitalData.Core.Tests.API", "DigitalData.Core.Tests.API\DigitalData.Core.Tests.API.csproj", "{9BC2DEC5-E89D-48CC-9A51-4D94496EE4A6}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Security", "Security", "{72CBAFBA-55CC-49C9-A484-F8F4550054CB}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigitalData.Core.Abstractions.Security", "DigitalData.Core.Abstractions.Security\DigitalData.Core.Abstractions.Security.csproj", "{C9266749-9504-4EA9-938F-F083357B60B7}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{EDF84A84-1E01-484E-B073-383F7139C891}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigitalData.Core.Infrastructure.AutoMapper", "DigitalData.Core.Infrastructure.AutoMapper\DigitalData.Core.Infrastructure.AutoMapper.csproj", "{CE00E1F7-2771-4D9C-88FB-E564894E539E}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Infrastructure", "Infrastructure", "{41795B74-A757-4E93-B907-83BFF04EEE5C}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -80,10 +92,36 @@ Global
|
||||
{9BC2DEC5-E89D-48CC-9A51-4D94496EE4A6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{9BC2DEC5-E89D-48CC-9A51-4D94496EE4A6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{9BC2DEC5-E89D-48CC-9A51-4D94496EE4A6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C9266749-9504-4EA9-938F-F083357B60B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C9266749-9504-4EA9-938F-F083357B60B7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C9266749-9504-4EA9-938F-F083357B60B7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C9266749-9504-4EA9-938F-F083357B60B7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{CE00E1F7-2771-4D9C-88FB-E564894E539E}.Debug|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{CE00E1F7-2771-4D9C-88FB-E564894E539E}.Debug|Any CPU.Build.0 = Release|Any CPU
|
||||
{CE00E1F7-2771-4D9C-88FB-E564894E539E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{CE00E1F7-2771-4D9C-88FB-E564894E539E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{A765EBEA-3D1E-4F36-869B-6D72F87FF3F6} = {41795B74-A757-4E93-B907-83BFF04EEE5C}
|
||||
{DB404CD9-CBB8-4771-AB1B-FD4FDE2C28CC} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
|
||||
{C57B2480-F632-4691-9C4C-8CC01237203C} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
|
||||
{B54DEF90-C30C-44EA-9875-76F1B330CBB7} = {EDF84A84-1E01-484E-B073-383F7139C891}
|
||||
{0B051A5F-BD38-47D1-BAFF-D44BA30D3FB7} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
|
||||
{6A80FFEC-9B83-40A7-8C78-124440B48B33} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
|
||||
{13E40DF1-6123-4838-9BF8-086C94E6ADF6} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
|
||||
{84B18026-F9A0-4366-BC69-1662D9E7342D} = {EDF84A84-1E01-484E-B073-383F7139C891}
|
||||
{E009A053-A9F4-48F2-984F-EF5C376A9B14} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
|
||||
{47D80C65-74A2-4EB8-96A5-D571A9108FB3} = {72CBAFBA-55CC-49C9-A484-F8F4550054CB}
|
||||
{0FA93730-8084-4907-B172-87D610323796} = {EDF84A84-1E01-484E-B073-383F7139C891}
|
||||
{9BC2DEC5-E89D-48CC-9A51-4D94496EE4A6} = {EDF84A84-1E01-484E-B073-383F7139C891}
|
||||
{72CBAFBA-55CC-49C9-A484-F8F4550054CB} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
|
||||
{C9266749-9504-4EA9-938F-F083357B60B7} = {72CBAFBA-55CC-49C9-A484-F8F4550054CB}
|
||||
{CE00E1F7-2771-4D9C-88FB-E564894E539E} = {41795B74-A757-4E93-B907-83BFF04EEE5C}
|
||||
{41795B74-A757-4E93-B907-83BFF04EEE5C} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {8E2C3187-F848-493A-9E79-56D20DDCAC94}
|
||||
EndGlobalSection
|
||||
|
||||
Reference in New Issue
Block a user