8 Commits

10 changed files with 242 additions and 10 deletions

View File

@@ -1,4 +1,5 @@
using DigitalData.Core.Security.Cryptographer;
using DigitalData.Core.Abstractions.Security;
using DigitalData.Core.Security.Cryptographer;
namespace DigitalData.Core.Security.Config
{
@@ -55,9 +56,14 @@ namespace DigitalData.Core.Security.Config
public IEnumerable<RSADecryptor> Decryptors { get; init; } = new List<RSADecryptor>();
public IEnumerable<TokenDescription> TokenDescriptions { get; init; } = new List<TokenDescription>();
public RSADecryptor? Vault { get; init; }
public AsymCryptParams() => AfterCreate += () =>
public AsymCryptParams()
{
// init decryptors
AfterCreate += () =>
{
// Create root folder if it does not exist
if (!Directory.Exists(PemDirectory))
@@ -92,5 +98,19 @@ namespace DigitalData.Core.Security.Config
}
}
};
// set signing credentials of token descriptions
AfterCreate += () =>
{
foreach(var tDesc in TokenDescriptions)
{
if (!Decryptors.TryGet(issuer: tDesc.Issuer, tDesc.Audience, out var decryptor) || decryptor is null)
throw new InvalidOperationException(
$"Decryptor for Issuer '{tDesc.Issuer}' and Audience '{tDesc.Audience}' could not be found or is null.");
tDesc.SigningCredentials = decryptor.CreateSigningCredentials(algorithm: tDesc.SigningAlgorithm, digest: tDesc.SigningDigest);
}
};
}
}
}

View File

@@ -0,0 +1,11 @@
using System.Security.Claims;
namespace DigitalData.Core.Security.Config
{
public class ClaimDescriptor<TPrincipal>
{
public Func<TPrincipal, IDictionary<string, object>>? CreateClaims { get; init; }
public Func<TPrincipal, ClaimsIdentity>? CreateSubject { get; init; }
}
}

View File

@@ -0,0 +1,13 @@
using AutoMapper;
using Microsoft.IdentityModel.Tokens;
namespace DigitalData.Core.Security.Config
{
public class MappingProfile : Profile
{
public MappingProfile()
{
CreateMap<TokenDescription, SecurityTokenDescriptor>();
}
}
}

View File

@@ -0,0 +1,90 @@
using Microsoft.IdentityModel.Tokens;
namespace DigitalData.Core.Security.Config
{
/// <summary>
/// Contains some information which used to create a security token. Designed to abstract <see cref="SecurityTokenDescriptor"/>
/// </summary>
public class TokenDescription
{
/// <summary>
/// Gets or sets the value of the 'audience' claim.
/// </summary>
public 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.
/// </summary>
public DateTime? Expires { get; set; }
/// <summary>
/// Gets or sets the issuer of this <see cref="ITokenDescription"/>.
/// </summary>
public 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 { get; set; }
/// <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 = null;
}
}

View File

@@ -1,4 +1,5 @@
using DigitalData.Core.Abstractions.Security;
using Microsoft.IdentityModel.Tokens;
using System.Reflection;
using System.Security.Cryptography;
@@ -24,8 +25,15 @@ namespace DigitalData.Core.Security.Cryptographer
public string Audience { get; init; } = string.Empty;
private readonly Lazy<RsaSecurityKey> _lazyRsaSecurityKey;
public RsaSecurityKey RsaSecurityKey => _lazyRsaSecurityKey.Value;
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
internal RSACryptographer() { }
internal RSACryptographer()
{
_lazyRsaSecurityKey = new(() => new RsaSecurityKey(RSA));
}
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
}
}

View File

@@ -1,5 +1,6 @@
using DigitalData.Core.Abstractions.Security;
using DigitalData.Core.Security.Config;
using Microsoft.IdentityModel.Tokens;
using System.Security.Cryptography;
namespace DigitalData.Core.Security.Cryptographer
@@ -59,5 +60,8 @@ namespace DigitalData.Core.Security.Cryptographer
}
private InvalidOperationException PemIsNullException => new($"Pem is null or empty. Issuer: {Issuer}, Audience: {Audience}.");
public SigningCredentials CreateSigningCredentials(string algorithm = SecurityAlgorithms.RsaSha256, string? digest = null)
=> digest is null ? new(RsaSecurityKey, algorithm) : new(RsaSecurityKey, algorithm, digest);
}
}

View File

@@ -4,18 +4,40 @@ using DigitalData.Core.Security.Cryptographer;
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 (bool Added, object Lock) _mappingProfile = (false, new());
private static IServiceCollection AddMappingProfile(this IServiceCollection services)
{
if (_mappingProfile.Added)
return services;
lock (_mappingProfile.Lock)
{
if (_mappingProfile.Added)
return services;
_mappingProfile.Added = true;
return services
.AddAutoMapper(typeof(MappingProfile).Assembly)
.AddSingleton<TokenDescriptorProvider>();
}
}
private static IServiceCollection AddParamsConfigureOptions<TParams>(this IServiceCollection services) where TParams : RSAFactoryParams
=> services.AddSingleton<IConfigureOptions<TParams>, ParamsConfigureOptions<TParams>>();
private static IServiceCollection AddAsymCryptService<TAsymCryptParams>(this IServiceCollection services, bool setAsDefault = false) where TAsymCryptParams : AsymCryptParams
=> setAsDefault
? services.AddParamsConfigureOptions<TAsymCryptParams>().AddSingleton<IAsymCryptService, AsymCryptService<TAsymCryptParams>>()
: services.AddParamsConfigureOptions<TAsymCryptParams>().AddSingleton<IAsymCryptService<TAsymCryptParams>, AsymCryptService<TAsymCryptParams>>();
{
services.AddParamsConfigureOptions<TAsymCryptParams>().AddMappingProfile();
return setAsDefault
? services.AddSingleton<IAsymCryptService, AsymCryptService<TAsymCryptParams>>()
: services.AddSingleton<IAsymCryptService<TAsymCryptParams>, AsymCryptService<TAsymCryptParams>>();
}
/// <summary>
/// Registers a custom asym crypt service with specified parameters from the given configuration section.
@@ -38,7 +60,7 @@ namespace DigitalData.Core.Security
/// <returns></returns>
public static IServiceCollection AddAsymCryptService(this IServiceCollection services, IConfigurationSection section, bool setAsDefault = false)
=> services.Configure<AsymCryptParams>(section).AddAsymCryptService<AsymCryptParams>(setAsDefault: setAsDefault);
/// <summary>
/// Registers an asym crypt service with the specified parameters from the given instance. Optionally, sets it as the default factory.
/// </summary>
@@ -68,8 +90,9 @@ namespace DigitalData.Core.Security
/// <returns>The updated <see cref="IServiceCollection"/> with the RSA Factory registered.</returns>
public static IServiceCollection AddRSAFactory(this IServiceCollection services, RSAFactoryParams? factoryParams = null) => services
.AddParamsConfigureOptions<RSAFactoryParams>()
.AddMappingProfile()
.AddScoped<IRSAFactory>(_ => new RSAFactory<RSAFactoryParams>(Options.Create(factoryParams ?? new())));
/// <summary>
/// Registers a custom RSA Factory with specified parameters from the given configuration section.
/// </summary>
@@ -81,7 +104,7 @@ namespace DigitalData.Core.Security
public static IServiceCollection AddRSAFactory<TRSAFactoryParams>(this IServiceCollection services, IConfigurationSection section, bool setAsDefault = false)
where TRSAFactoryParams : RSAFactoryParams
{
services.AddParamsConfigureOptions<TRSAFactoryParams>().Configure<TRSAFactoryParams>(section);
services.AddMappingProfile().AddParamsConfigureOptions<TRSAFactoryParams>().Configure<TRSAFactoryParams>(section);
return setAsDefault
? services.AddSingleton<IRSAFactory, RSAFactory<TRSAFactoryParams>>()
: services.AddSingleton<IRSAFactory<TRSAFactoryParams>, RSAFactory<TRSAFactoryParams>>();
@@ -98,10 +121,23 @@ namespace DigitalData.Core.Security
public static IServiceCollection AddRSAFactory<TRSAFactoryParams>(this IServiceCollection services, TRSAFactoryParams rsaParams, bool setAsDefault = false)
where TRSAFactoryParams : RSAFactoryParams
{
services.AddSingleton(Options.Create(rsaParams));
services.AddMappingProfile().AddSingleton(Options.Create(rsaParams));
return setAsDefault
? services.AddParamsConfigureOptions<TRSAFactoryParams>().AddSingleton<IRSAFactory, RSAFactory<TRSAFactoryParams>>()
: services.AddParamsConfigureOptions<TRSAFactoryParams>().AddSingleton<IRSAFactory<TRSAFactoryParams>, RSAFactory<TRSAFactoryParams>>();
}
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));
}
}
}

View File

@@ -7,6 +7,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AutoMapper" Version="13.0.1" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
</ItemGroup>

View File

@@ -0,0 +1,30 @@
using DigitalData.Core.Security.Config;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
namespace DigitalData.Core.Security
{
public class JwtSignatureService<TPrincipal> : JwtSecurityTokenHandler
{
private readonly ClaimDescriptor<TPrincipal> _claimDescriptor;
private readonly TokenDescriptorProvider _descriptorProvider;
public JwtSignatureService(IOptions<ClaimDescriptor<TPrincipal>> claimDescriptorOptions, TokenDescriptorProvider descriptorProvider)
{
_claimDescriptor = claimDescriptorOptions.Value;
_descriptorProvider = descriptorProvider;
}
public SecurityToken CreateToken(TPrincipal subject, TokenDescription description)
{
var descriptor = _descriptorProvider.Create(description: description);
descriptor.Claims = _claimDescriptor.CreateClaims?.Invoke(subject);
descriptor.Subject = _claimDescriptor.CreateSubject?.Invoke(subject);
return CreateToken(descriptor);
}
public string CreateAndWriteToken(TPrincipal subject, TokenDescription description) => WriteToken(CreateToken(subject, description));
}
}

View File

@@ -0,0 +1,19 @@
using AutoMapper;
using DigitalData.Core.Security.Config;
using Microsoft.IdentityModel.Tokens;
namespace DigitalData.Core.Security
{
public class TokenDescriptorProvider
{
private readonly IMapper _mapper;
public TokenDescriptorProvider(IMapper mapper)
{
_mapper = mapper;
}
public SecurityTokenDescriptor Create(TokenDescription description)
=> _mapper.Map(description, new SecurityTokenDescriptor());
}
}