Still working on getting client credential flow complete, most of the request works but still working on generating the JWT.
This commit is contained in:
parent
1a8c63808a
commit
8782ef39c6
80 changed files with 1331 additions and 414 deletions
|
|
@ -1,9 +1,22 @@
|
|||
using System.Text;
|
||||
using IdentityShroud.Core.Model;
|
||||
using IdentityShroud.Core.Security;
|
||||
|
||||
namespace IdentityShroud.Core.Contracts;
|
||||
|
||||
public interface IDataEncryptionService
|
||||
{
|
||||
EncryptedValue Encrypt(ReadOnlySpan<byte> plain);
|
||||
byte[] Decrypt(EncryptedValue input);
|
||||
EncryptedValue Encrypt(RealmDek dek, ReadOnlySpan<byte> plain);
|
||||
byte[] Decrypt(IReadOnlyList<RealmDek> deks, EncryptedValue input);
|
||||
}
|
||||
|
||||
public static class DataEncryptionServiceExtensions
|
||||
{
|
||||
public static string DecryptUtf8ToString(
|
||||
this IDataEncryptionService des,
|
||||
IReadOnlyList<RealmDek> deks,
|
||||
EncryptedValue input)
|
||||
{
|
||||
return Encoding.UTF8.GetString(des.Decrypt(deks, input));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,10 @@
|
|||
using IdentityShroud.Core.Messages;
|
||||
using IdentityShroud.Core.Model;
|
||||
using IdentityShroud.Core.Security.Keys;
|
||||
|
||||
namespace IdentityShroud.Core.Contracts;
|
||||
|
||||
public record CreateKeyResponse(KeyType KeyType, KeyData Key);
|
||||
|
||||
public interface IKeyService
|
||||
{
|
||||
RealmKey CreateKey(KeyPolicy policy);
|
||||
|
||||
JsonWebKey? CreateJsonWebKey(RealmKey realmKey);
|
||||
CreateKeyResponse CreateKey(KeyPolicy policy);
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
using IdentityShroud.Core.Messages.Realm;
|
||||
using IdentityShroud.Core.Model;
|
||||
using IdentityShroud.Core.Services;
|
||||
|
||||
namespace IdentityShroud.Core.Contracts;
|
||||
|
||||
|
|
@ -9,7 +8,7 @@ public interface IRealmService
|
|||
Task<Realm?> FindById(Guid id, CancellationToken ct = default);
|
||||
Task<Realm?> FindBySlug(string slug, CancellationToken ct = default);
|
||||
|
||||
Task<Result<RealmCreateResponse>> Create(RealmCreateRequest request, CancellationToken ct = default);
|
||||
Task<Result<Realm>> Create(RealmCreateRequest request, CancellationToken ct = default);
|
||||
Task LoadActiveKeys(Realm realm);
|
||||
Task LoadDeks(Realm realm);
|
||||
}
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
namespace IdentityShroud.Core.Contracts;
|
||||
|
||||
public class ClientCreateRequest
|
||||
{
|
||||
public required string ClientId { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public string? SignatureAlgorithm { get; set; }
|
||||
public bool? AllowClientCredentialsFlow { get; set; }
|
||||
}
|
||||
public record ClientCreateRequest(
|
||||
string ClientId,
|
||||
string? Name = null,
|
||||
string? Description = null,
|
||||
string? SignatureAlgorithm = null,
|
||||
bool Confidential = false,
|
||||
bool AllowClientCredentialsFlow = false,
|
||||
bool GenerateSecret = false);
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Text.Json.Serialization;
|
||||
using IdentityShroud.Core.Helpers;
|
||||
using IdentityShroud.Core.Security.Keys;
|
||||
|
||||
namespace IdentityShroud.Core.Messages;
|
||||
|
||||
|
|
@ -9,7 +10,7 @@ namespace IdentityShroud.Core.Messages;
|
|||
public class JsonWebKey
|
||||
{
|
||||
[JsonPropertyName("kty")]
|
||||
public string KeyType { get; set; } = "RSA";
|
||||
public required KeyType KeyType { get; set; }
|
||||
|
||||
// Common values sig(nature) enc(ryption)
|
||||
[JsonPropertyName("use")]
|
||||
|
|
|
|||
9
IdentityShroud.Core/DTO/OpenId/GrantTypes.cs
Normal file
9
IdentityShroud.Core/DTO/OpenId/GrantTypes.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace IdentityShroud.Core.DTO.OpenId;
|
||||
|
||||
public enum GrantTypes
|
||||
{
|
||||
[JsonStringEnumMemberName("client_credentials")]
|
||||
ClientCredentials
|
||||
}
|
||||
19
IdentityShroud.Core/DTO/OpenId/TokenResponse.cs
Normal file
19
IdentityShroud.Core/DTO/OpenId/TokenResponse.cs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace IdentityShroud.Core.Services.OpenId;
|
||||
|
||||
public class TokenResponse
|
||||
{
|
||||
[JsonPropertyName("access_token")]
|
||||
public required string AccessToken { get; set; }
|
||||
|
||||
[JsonPropertyName("token_type")]
|
||||
public required string TokenType { get; set; }
|
||||
|
||||
[JsonPropertyName("expires_in")]
|
||||
public int? ExpiresIn { get; set; }
|
||||
|
||||
[JsonPropertyName("refresh_token")]
|
||||
public string? RefreshToken { get; set; }
|
||||
|
||||
}
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
namespace IdentityShroud.Core.Messages.Realm;
|
||||
|
||||
public record RealmCreateRequest(Guid? Id, string? Slug, string Name);
|
||||
public record RealmCreateRequest(Guid? Id = null, string? Slug = null, string? Name = null);
|
||||
|
|
@ -1,12 +1,6 @@
|
|||
using IdentityShroud.Core.Security;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
namespace IdentityShroud.Core;
|
||||
namespace IdentityShroud.Core.EFCore;
|
||||
|
||||
public class DekIdConverter : ValueConverter<DekId, Guid>
|
||||
{
|
||||
public DekIdConverter()
|
||||
: base(id => id.Id, guid => new DekId(guid))
|
||||
{
|
||||
}
|
||||
}
|
||||
public class DekIdConverter() : ValueConverter<DekId, Guid>(id => id.Id, guid => new DekId(guid));
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
namespace IdentityShroud.Core.EFCore;
|
||||
|
||||
public class DictionaryToJsonConverter<TKey, TValue> : ValueConverter<Dictionary<TKey, TValue>, string>
|
||||
where TKey : notnull
|
||||
{
|
||||
public DictionaryToJsonConverter() : base(
|
||||
v => JsonSerializer.Serialize(v),
|
||||
v => JsonSerializer.Deserialize<Dictionary<TKey, TValue>>(v) ?? new())
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
namespace IdentityShroud.Core.EFCore;
|
||||
|
||||
public class JwtSigAlgNameConverter() : ValueConverter<JwtSigAlgName, string>(j => j.ToString(), s => new(s));
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
using IdentityShroud.Core.Security;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
namespace IdentityShroud.Core;
|
||||
namespace IdentityShroud.Core.EFCore;
|
||||
|
||||
public class KekIdConverter : ValueConverter<KekId, Guid>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
using IdentityShroud.Core.Security.Keys;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
namespace IdentityShroud.Core.EFCore;
|
||||
|
||||
public class KeyTypeConverter() : ValueConverter<KeyType, string>(id => id.ToString(), s => new(s));
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
using IdentityShroud.Core.Model;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
namespace IdentityShroud.Core.EFCore;
|
||||
|
||||
public class RealmSigningKeyIdConverter : ValueConverter<RealmSigningKeyId, Guid>
|
||||
{
|
||||
public RealmSigningKeyIdConverter()
|
||||
: base(id => id.Id, guid => new RealmSigningKeyId(guid))
|
||||
{
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
using System.Linq.Expressions;
|
||||
using IdentityShroud.Core.Model;
|
||||
using IdentityShroud.Core.Security;
|
||||
using IdentityShroud.Core.Security.Keys;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace IdentityShroud.Core;
|
||||
namespace IdentityShroud.Core.EFCore;
|
||||
|
||||
public class DbConfiguration
|
||||
{
|
||||
|
|
@ -20,7 +20,7 @@ public class Db(
|
|||
{
|
||||
public virtual DbSet<Client> Clients { get; set; }
|
||||
public virtual DbSet<Realm> Realms { get; set; }
|
||||
public virtual DbSet<RealmKey> Keys { get; set; }
|
||||
public virtual DbSet<RealmSigningKey> Keys { get; set; }
|
||||
public virtual DbSet<RealmDek> Deks { get; set; }
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
|
|
@ -50,6 +50,10 @@ public class Db(
|
|||
base.ConfigureConventions(b);
|
||||
|
||||
b.Properties<DekId>().HaveConversion<DekIdConverter>();
|
||||
b.Properties<Dictionary<string, string>>().HaveConversion<DictionaryToJsonConverter<string, string>>();
|
||||
b.Properties<JwtSigAlgName>().HaveConversion<JwtSigAlgNameConverter>();
|
||||
b.Properties<KekId>().HaveConversion<KekIdConverter>();
|
||||
b.Properties<KeyType>().HaveConversion<KeyTypeConverter>();
|
||||
b.Properties<RealmSigningKeyId>().HaveConversion<RealmSigningKeyIdConverter>();
|
||||
}
|
||||
}
|
||||
|
|
@ -10,12 +10,12 @@
|
|||
<PackageReference Include="EFCore.NamingConventions" Version="10.0.1" />
|
||||
<PackageReference Include="FluentResults" Version="4.0.0" />
|
||||
<PackageReference Include="FluentValidation" Version="12.1.1" />
|
||||
<PackageReference Include="jose-jwt" Version="5.2.0" />
|
||||
<PackageReference Include="LanguageExt.Core" Version="4.4.9" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.3.9" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="10.0.2" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
|
||||
<PackageReference Include="Shouldly" Version="4.3.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -19,8 +19,16 @@ public class Client
|
|||
public string? Description { get; set; }
|
||||
|
||||
[MaxLength(20)]
|
||||
public string? SignatureAlgorithm { get; set; }
|
||||
public JwtSigAlgName? SignatureAlgorithm { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables confidential flows
|
||||
/// </summary>
|
||||
public bool Confidential { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables the client credentials flow which required Confidential to be true too.
|
||||
/// </summary>
|
||||
public bool AllowClientCredentialsFlow { get; set; } = false;
|
||||
|
||||
public required DateTime CreatedAt { get; set; }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using IdentityShroud.Core.Contracts;
|
||||
using IdentityShroud.Core.Security;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
|
@ -14,8 +13,9 @@ public class ClientSecret
|
|||
public int Id { get; set; }
|
||||
public Guid ClientId { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime? Expires { get; set; }
|
||||
public DateTime? RevokedAt { get; set; }
|
||||
public EncryptedValue? Secret { get; set; }
|
||||
public required EncryptedValue Secret { get; set; }
|
||||
}
|
||||
|
||||
public class ClientSecretConfiguration : IEntityTypeConfiguration<ClientSecret>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using IdentityShroud.Core.Security;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace IdentityShroud.Core.Model;
|
||||
|
||||
|
|
@ -20,12 +17,17 @@ public class Realm
|
|||
public string Name { get; set; } = "";
|
||||
public List<Client> Clients { get; init; } = [];
|
||||
|
||||
public List<RealmKey> Keys { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Note multiple keys can be in use at the same time because different clients may be configured to use
|
||||
/// a different keytype depending on their clients requirements/capabilities.
|
||||
/// </summary>
|
||||
public List<RealmSigningKey> TokenSigningKeys { get; init; } = [];
|
||||
|
||||
public List<RealmDek> Deks { get; init; } = [];
|
||||
public List<RealmDek> DataEncryptionKeys { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Can be overriden per client
|
||||
/// </summary>
|
||||
public string DefaultSignatureAlgorithm { get; set; } = JsonWebAlgorithm.RS256;
|
||||
public JwtSigAlgName DefaultSignatureAlgorithm { get; set; } = JwtSigAlgName.RS256;
|
||||
}
|
||||
|
|
@ -1,16 +1,18 @@
|
|||
using IdentityShroud.Core.Security;
|
||||
using IdentityShroud.Core.Security.Keys;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace IdentityShroud.Core.Model;
|
||||
|
||||
|
||||
public record RealmDek
|
||||
{
|
||||
public required DekId Id { get; init; }
|
||||
public required bool Active { get; set; }
|
||||
public required string Algorithm { get; init; }
|
||||
public required KeyType Algorithm { get; init; }
|
||||
public required EncryptedDek KeyData { get; init; }
|
||||
public required Guid RealmId { get; init; }
|
||||
public Guid RealmId { get; init; }
|
||||
}
|
||||
|
||||
public class RealmDekConfiguration : IEntityTypeConfiguration<RealmDek>
|
||||
|
|
@ -21,4 +23,5 @@ public class RealmDekConfiguration : IEntityTypeConfiguration<RealmDek>
|
|||
b.HasKey(e => e.Id);
|
||||
b.ComplexProperty(e => e.KeyData, e => e.IsRequired());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,33 +1,34 @@
|
|||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using IdentityShroud.Core.Contracts;
|
||||
using IdentityShroud.Core.Security;
|
||||
using IdentityShroud.Core.Security.Keys;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace IdentityShroud.Core.Model;
|
||||
|
||||
public record RealmKey
|
||||
public record RealmSigningKey
|
||||
{
|
||||
public required Guid Id { get; init; }
|
||||
public required string KeyType { get; init; }
|
||||
public required RealmSigningKeyId Id { get; init; }
|
||||
public required KeyType KeyType { get; init; }
|
||||
public required EncryptedDek Key { get; init; }
|
||||
public required DateTime CreatedAt { get; init; }
|
||||
public DateTime? RevokedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Key with highest priority will be used. While there is not really a use case for this I know some users
|
||||
/// are more comfortable replacing keys by using priority then directly deactivating the old key.
|
||||
/// </summary>
|
||||
public int Priority { get; set; } = 10;
|
||||
|
||||
public Dictionary<string, string>? PublicKeyParameters { get; set; }
|
||||
}
|
||||
|
||||
public class RealmKeyConfiguration : IEntityTypeConfiguration<RealmKey>
|
||||
public class RealmKeyConfiguration : IEntityTypeConfiguration<RealmSigningKey>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<RealmKey> b)
|
||||
public void Configure(EntityTypeBuilder<RealmSigningKey> b)
|
||||
{
|
||||
b.ToTable("realm_key");
|
||||
b.HasKey(e => e.Id);
|
||||
|
||||
b.ComplexProperty(e => e.Key, e => e.IsRequired());
|
||||
b.Property(e => e.PublicKeyParameters).HasColumnType("jsonb");
|
||||
}
|
||||
}
|
||||
}
|
||||
24
IdentityShroud.Core/Model/RealmSigningKeyId.cs
Normal file
24
IdentityShroud.Core/Model/RealmSigningKeyId.cs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace IdentityShroud.Core.Model;
|
||||
|
||||
[JsonConverter(typeof(RealmSigningKeyIdJsonConverter))]
|
||||
public readonly record struct RealmSigningKeyId(Guid Id)
|
||||
{
|
||||
public override string ToString() => Id.ToString("N");
|
||||
|
||||
public static RealmSigningKeyId NewId()
|
||||
{
|
||||
return new(Guid.NewGuid());
|
||||
}
|
||||
}
|
||||
|
||||
public class RealmSigningKeyIdJsonConverter : JsonConverter<RealmSigningKeyId>
|
||||
{
|
||||
public override RealmSigningKeyId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
=> new (reader.GetGuid());
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, RealmSigningKeyId value, JsonSerializerOptions options)
|
||||
=> writer.WriteStringValue(value.ToString());
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
namespace IdentityShroud.Core.Security;
|
||||
|
||||
public record struct DekId(Guid Id)
|
||||
public readonly record struct DekId(Guid Id)
|
||||
{
|
||||
public static DekId NewId() => new(Guid.NewGuid());
|
||||
|
||||
public override string ToString() => Id.ToString("N");
|
||||
}
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace IdentityShroud.Core.Security;
|
||||
|
||||
public record EncryptedDek(KekId KekId, byte[] Value);
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace IdentityShroud.Core.Security;
|
||||
|
||||
public record EncryptedValue(DekId DekId, byte[] Value);
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ namespace IdentityShroud.Core.Security;
|
|||
|
||||
public static class Encryption
|
||||
{
|
||||
private record struct AlgVersion(int Version, int NonceSize, int TagSize);
|
||||
private readonly record struct AlgVersion(int Version, int NonceSize, int TagSize);
|
||||
|
||||
private static AlgVersion[] _versions =
|
||||
[
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
namespace IdentityShroud.Core.Security;
|
||||
|
||||
public static class JsonWebAlgorithm
|
||||
{
|
||||
public const string RS256 = "RS256";
|
||||
}
|
||||
26
IdentityShroud.Core/Security/Jwt/IJwtSignatureProvider.cs
Normal file
26
IdentityShroud.Core/Security/Jwt/IJwtSignatureProvider.cs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
using System.Text.Json;
|
||||
|
||||
namespace IdentityShroud.Core;
|
||||
|
||||
public interface IJwtSignatureProvider : IDisposable
|
||||
{
|
||||
/*
|
||||
Of the signature and MAC algorithms specified in JSON Web Algorithms
|
||||
[JWA], only HMAC SHA-256 ("HS256") and "none" MUST be implemented by
|
||||
conforming JWT implementations. It is RECOMMENDED that
|
||||
implementations also support RSASSA-PKCS1-v1_5 with the SHA-256 hash
|
||||
algorithm ("RS256") and ECDSA using the P-256 curve and the SHA-256
|
||||
hash algorithm ("ES256"). Support for other algorithms and key sizes
|
||||
is OPTIONAL.
|
||||
*/
|
||||
|
||||
void WriteJwtHeaderFields(Utf8JsonWriter writer);
|
||||
/// <summary>
|
||||
/// Length of the binary signature in bytes.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
int GetSignatureLength();
|
||||
|
||||
void CalculateSignature(ReadOnlySpan<byte> jwt, Span<byte> signatureOut);
|
||||
|
||||
}
|
||||
23
IdentityShroud.Core/Security/Jwt/JwtSigAlgName.cs
Normal file
23
IdentityShroud.Core/Security/Jwt/JwtSigAlgName.cs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace IdentityShroud.Core;
|
||||
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
public readonly record struct JwtSigAlgName(string Name) : IEquatable<JwtSigAlgName>
|
||||
{
|
||||
// HMAC using SHA-???
|
||||
public static JwtSigAlgName HS256 => new("HS256"); // REQUIRED
|
||||
public static JwtSigAlgName HS384 => new("HS384");
|
||||
public static JwtSigAlgName HS512 => new("HS512");
|
||||
|
||||
// RSASSA-PKCS1-v1_5 using SHA-???
|
||||
public static JwtSigAlgName RS256 => new("RS256");
|
||||
public static JwtSigAlgName RS384 => new("RS384");
|
||||
public static JwtSigAlgName RS512 => new("RS512");
|
||||
|
||||
public static JwtSigAlgName ES256 => new("ES256"); // ECDSA using P-256 and SHA-256
|
||||
public static JwtSigAlgName ES384 => new("ES384"); // ECDSA using P-384 and SHA-384
|
||||
public static JwtSigAlgName ES512 => new("ES512"); // ECDSA using P-521 and SHA-512
|
||||
|
||||
public override string ToString() => Name;
|
||||
}
|
||||
100
IdentityShroud.Core/Security/Jwt/JwtSignatureGenerator.cs
Normal file
100
IdentityShroud.Core/Security/Jwt/JwtSignatureGenerator.cs
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
using System.Buffers.Text;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
|
||||
namespace IdentityShroud.Core;
|
||||
|
||||
public static class JwtSignatureGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates a JWT signature using RS256 algorithm
|
||||
/// </summary>
|
||||
/// <param name="headerBase64Url">Base64Url encoded header</param>
|
||||
/// <param name="payloadBase64Url">Base64Url encoded payload</param>
|
||||
/// <param name="privateKey">RSA private key (PEM format or RSA parameters)</param>
|
||||
/// <returns>Base64Url encoded signature</returns>
|
||||
public static string GenerateRS256Signature(string headerBase64Url, string payloadBase64Url, RSA privateKey)
|
||||
{
|
||||
// Combine header and payload with a period
|
||||
string dataToSign = $"{headerBase64Url}.{payloadBase64Url}";
|
||||
|
||||
// Convert to bytes
|
||||
byte[] dataBytes = Encoding.UTF8.GetBytes(dataToSign);
|
||||
|
||||
// Sign the data using RSA-SHA256
|
||||
byte[] signatureBytes = privateKey.SignData(dataBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||
|
||||
// Convert signature to Base64Url encoding
|
||||
string signature = WebEncoders.Base64UrlEncode(signatureBytes);
|
||||
|
||||
return signature;
|
||||
}
|
||||
|
||||
public static string GenerateCompleteJwt(string headerBase64Url, string payloadBase64Url, RSA privateKey)
|
||||
{
|
||||
string signature = GenerateRS256Signature(headerBase64Url, payloadBase64Url, privateKey);
|
||||
return $"{headerBase64Url}.{payloadBase64Url}.{signature}";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class JwtCreator
|
||||
{
|
||||
|
||||
public static byte[] CreateEncodedJwt(ReadOnlySpan<byte> payloadUtf8, IJwtSignatureProvider signatureProvider)
|
||||
{
|
||||
MemoryStream memStream = new();
|
||||
Utf8JsonWriter writer = new(memStream);
|
||||
WriteJwtHeader(writer, signatureProvider);
|
||||
writer.Flush();
|
||||
memStream.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
int headerBase64Length = Base64Url.GetEncodedLength((int)memStream.Length);
|
||||
int payloadBase64Length = Base64Url.GetEncodedLength(payloadUtf8.Length);
|
||||
int signatureBase64Length = Base64Url.GetEncodedLength(signatureProvider.GetSignatureLength());
|
||||
int totalLength = headerBase64Length + 1 + payloadBase64Length + 1 + signatureBase64Length;
|
||||
|
||||
var completeJwt = new byte[totalLength];
|
||||
|
||||
//
|
||||
var byteArray = new byte[memStream.Length];
|
||||
memStream.ReadExactly(byteArray, 0, (int)memStream.Length);
|
||||
int written = Base64Url.EncodeToUtf8(byteArray, completeJwt);
|
||||
|
||||
if (written != headerBase64Length)
|
||||
throw new Exception("expected header length did not match bytes written");
|
||||
|
||||
completeJwt[headerBase64Length] = (byte)'.';
|
||||
|
||||
written = Base64Url.EncodeToUtf8(payloadUtf8, completeJwt.AsSpan().Slice(headerBase64Length + 1, payloadBase64Length));
|
||||
|
||||
if (written != payloadBase64Length)
|
||||
throw new Exception("expected payload length did not match bytes written");
|
||||
|
||||
completeJwt[headerBase64Length + 1 + payloadBase64Length] = (byte)'.';
|
||||
|
||||
|
||||
Span<byte> signature = stackalloc byte[signatureProvider.GetSignatureLength()];
|
||||
signatureProvider.CalculateSignature(
|
||||
completeJwt.AsSpan().Slice(0, headerBase64Length + 1 + payloadBase64Length),
|
||||
signature);
|
||||
|
||||
written = Base64Url.EncodeToUtf8(signature, completeJwt.AsSpan()
|
||||
.Slice(headerBase64Length + 1 + payloadBase64Length + 1));
|
||||
|
||||
if (written != signatureBase64Length)
|
||||
throw new Exception("expected signature length did not match bytes written");
|
||||
|
||||
return completeJwt;
|
||||
}
|
||||
|
||||
private static void WriteJwtHeader(Utf8JsonWriter writer, IJwtSignatureProvider signatureProvider)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString("typ"u8, "JWT"u8);
|
||||
signatureProvider.WriteJwtHeaderFields(writer);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
}
|
||||
88
IdentityShroud.Core/Security/Jwt/RsaJwtSignatureProvider.cs
Normal file
88
IdentityShroud.Core/Security/Jwt/RsaJwtSignatureProvider.cs
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
using IdentityShroud.Core.Model;
|
||||
using IdentityShroud.Core.Services;
|
||||
|
||||
namespace IdentityShroud.Core;
|
||||
|
||||
public class SignatureProviderFactory(DekEncryptionService dekCryptor, IServiceProvider services)
|
||||
{
|
||||
|
||||
public static void SelectAlgorithmAndKey(Realm realm, Client client, out JwtSigAlgName alg, out byte[] key)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public IJwtSignatureProvider Create(JwtSigAlgName algorithm, byte[] keyData)
|
||||
{
|
||||
|
||||
|
||||
//realm.DefaultSignatureAlgorithm
|
||||
//realm.TokenSigningKeys
|
||||
|
||||
//IJwtSignatureProvider? sigProvider = services.GetKeyedService<IJwtSignatureProvider>();
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class RsaJwtSignatureProvider : IJwtSignatureProvider
|
||||
{
|
||||
private JwtSigAlgName _sigAlgName;
|
||||
private RealmSigningKeyId _keyId;
|
||||
private readonly RSA _rsa;
|
||||
|
||||
public RsaJwtSignatureProvider(DekEncryptionService dekCryptor,
|
||||
RealmSigningKey privateKey,
|
||||
JwtSigAlgName sigAlgName)
|
||||
{
|
||||
_sigAlgName = sigAlgName;
|
||||
_keyId = privateKey.Id;
|
||||
|
||||
byte[] key = dekCryptor.Decrypt(privateKey.Key);
|
||||
_rsa = RSA.Create();
|
||||
_rsa.ImportPkcs8PrivateKey(key, out int _);
|
||||
}
|
||||
|
||||
/*
|
||||
+-------------------+---------------------------------+
|
||||
| "alg" Param Value | Digital Signature Algorithm |
|
||||
+-------------------+---------------------------------+
|
||||
| RS256 | RSASSA-PKCS1-v1_5 using SHA-256 |
|
||||
| RS384 | RSASSA-PKCS1-v1_5 using SHA-384 |
|
||||
| RS512 | RSASSA-PKCS1-v1_5 using SHA-512 |
|
||||
+-------------------+---------------------------------+
|
||||
*/
|
||||
|
||||
|
||||
public void WriteJwtHeaderFields(Utf8JsonWriter writer)
|
||||
{
|
||||
writer.WriteString("alg"u8, _sigAlgName.ToString());
|
||||
writer.WriteString("kid"u8, _keyId.ToString());
|
||||
}
|
||||
|
||||
public int GetSignatureLength()
|
||||
{
|
||||
return _rsa.KeySize / 8;
|
||||
}
|
||||
|
||||
public void CalculateSignature(ReadOnlySpan<byte> jwt, Span<byte> sig)
|
||||
{
|
||||
_rsa.SignData(jwt, sig, GetHashAlgorithmName(), RSASignaturePadding.Pkcs1);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_rsa.Dispose();
|
||||
}
|
||||
|
||||
private HashAlgorithmName GetHashAlgorithmName()
|
||||
=> _sigAlgName.Name switch
|
||||
{
|
||||
"RS256" => HashAlgorithmName.SHA256,
|
||||
"RS384" => HashAlgorithmName.SHA384,
|
||||
"RS512" => HashAlgorithmName.SHA512,
|
||||
_ => throw new ArgumentException("Invalid algorithm for RsaJwtSignatureProvider")
|
||||
};
|
||||
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
|
||||
namespace IdentityShroud.Core;
|
||||
|
||||
public static class JwtSignatureGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates a JWT signature using RS256 algorithm
|
||||
/// </summary>
|
||||
/// <param name="headerBase64Url">Base64Url encoded header</param>
|
||||
/// <param name="payloadBase64Url">Base64Url encoded payload</param>
|
||||
/// <param name="privateKey">RSA private key (PEM format or RSA parameters)</param>
|
||||
/// <returns>Base64Url encoded signature</returns>
|
||||
public static string GenerateRS256Signature(string headerBase64Url, string payloadBase64Url, RSA privateKey)
|
||||
{
|
||||
// Combine header and payload with a period
|
||||
string dataToSign = $"{headerBase64Url}.{payloadBase64Url}";
|
||||
|
||||
// Convert to bytes
|
||||
byte[] dataBytes = Encoding.UTF8.GetBytes(dataToSign);
|
||||
|
||||
// Sign the data using RSA-SHA256
|
||||
byte[] signatureBytes = privateKey.SignData(dataBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||
|
||||
// Convert signature to Base64Url encoding
|
||||
string signature = WebEncoders.Base64UrlEncode(signatureBytes);
|
||||
|
||||
return signature;
|
||||
}
|
||||
|
||||
public static string GenerateCompleteJwt(string headerBase64Url, string payloadBase64Url, RSA privateKey)
|
||||
{
|
||||
string signature = GenerateRS256Signature(headerBase64Url, payloadBase64Url, privateKey);
|
||||
return $"{headerBase64Url}.{payloadBase64Url}.{signature}";
|
||||
}
|
||||
}
|
||||
10
IdentityShroud.Core/Security/Keys/Aes/AesKeyPolicy.cs
Normal file
10
IdentityShroud.Core/Security/Keys/Aes/AesKeyPolicy.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
namespace IdentityShroud.Core.Security.Keys.Aes;
|
||||
|
||||
public class AesKeyPolicy : KeyPolicy
|
||||
{
|
||||
public AesKeyPolicy()
|
||||
{
|
||||
KeyType = KeyType.AES;
|
||||
KeySize = 256;
|
||||
}
|
||||
}
|
||||
19
IdentityShroud.Core/Security/Keys/Aes/AesProvider.cs
Normal file
19
IdentityShroud.Core/Security/Keys/Aes/AesProvider.cs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
using System.Security.Cryptography;
|
||||
using IdentityShroud.Core.Messages;
|
||||
|
||||
namespace IdentityShroud.Core.Security.Keys.Aes;
|
||||
|
||||
public class AesProvider : IKeyProvider
|
||||
{
|
||||
public bool IsPublic => false;
|
||||
public KeyData CreateKey(KeyPolicy policy)
|
||||
{
|
||||
return new KeyData(RandomNumberGenerator.GetBytes(policy.KeySize / 8));
|
||||
}
|
||||
|
||||
public void SetJwkParameters(Dictionary<string, string> parameters, JsonWebKey jwk)
|
||||
{
|
||||
// Can we use this for Jwe?
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
|
@ -2,17 +2,32 @@ using IdentityShroud.Core.Messages;
|
|||
|
||||
namespace IdentityShroud.Core.Security.Keys;
|
||||
|
||||
public abstract class KeyPolicy
|
||||
public class KeyPolicy
|
||||
{
|
||||
public abstract string KeyType { get; }
|
||||
public KeyType KeyType { get; protected init; }
|
||||
public int KeySize { get; protected init; }
|
||||
}
|
||||
|
||||
public record KeyData(byte[] PrivateKey, Dictionary<string, string>? PublicKeyParameters = null)
|
||||
{
|
||||
/// <summary>
|
||||
/// The data to be kept private, also used for symmetric keys
|
||||
/// </summary>
|
||||
public byte[] PrivateKey { get; set; } = PrivateKey;
|
||||
|
||||
public Dictionary<string, string>? PublicKeyParameters { get; set; } = PublicKeyParameters;
|
||||
}
|
||||
|
||||
|
||||
public interface IKeyProvider
|
||||
{
|
||||
byte[] CreateKey(KeyPolicy policy);
|
||||
/// <summary>
|
||||
/// Returns true when this key uses public key cryptography
|
||||
/// </summary>
|
||||
bool IsPublic { get; }
|
||||
KeyData CreateKey(KeyPolicy policy);
|
||||
|
||||
void SetJwkParameters(byte[] key, JsonWebKey jwk);
|
||||
void SetJwkParameters(Dictionary<string, string> parameters, JsonWebKey jwk);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3,5 +3,5 @@ namespace IdentityShroud.Core.Security.Keys;
|
|||
|
||||
public interface IKeyProviderFactory
|
||||
{
|
||||
public IKeyProvider CreateProvider(string keyType);
|
||||
public IKeyProvider CreateProvider(KeyType keyType);
|
||||
}
|
||||
|
|
@ -1,15 +1,18 @@
|
|||
using IdentityShroud.Core.Security.Keys.Aes;
|
||||
using IdentityShroud.Core.Security.Keys.Rsa;
|
||||
|
||||
namespace IdentityShroud.Core.Security.Keys;
|
||||
|
||||
public class KeyProviderFactory : IKeyProviderFactory
|
||||
{
|
||||
public IKeyProvider CreateProvider(string keyType)
|
||||
public IKeyProvider CreateProvider(KeyType keyType)
|
||||
{
|
||||
switch (keyType)
|
||||
switch (keyType.Name)
|
||||
{
|
||||
case "RSA":
|
||||
return new RsaProvider();
|
||||
case "AES":
|
||||
return new AesProvider();
|
||||
default:
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
|
|
|||
21
IdentityShroud.Core/Security/Keys/KeyType.cs
Normal file
21
IdentityShroud.Core/Security/Keys/KeyType.cs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace IdentityShroud.Core.Security.Keys;
|
||||
|
||||
[JsonConverter(typeof(KeyTypeJsonConverter))]
|
||||
public readonly record struct KeyType(string Name)
|
||||
{
|
||||
public static KeyType AES => new("AES");
|
||||
public static KeyType RSA => new("RSA");
|
||||
public override string ToString() => Name;
|
||||
}
|
||||
|
||||
public class KeyTypeJsonConverter : JsonConverter<KeyType>
|
||||
{
|
||||
public override KeyType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
=> new KeyType(reader.GetString()!);
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, KeyType value, JsonSerializerOptions options)
|
||||
=> writer.WriteStringValue(value.ToString());
|
||||
}
|
||||
10
IdentityShroud.Core/Security/Keys/Rsa/RsaKeyPolicy.cs
Normal file
10
IdentityShroud.Core/Security/Keys/Rsa/RsaKeyPolicy.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
namespace IdentityShroud.Core.Security.Keys.Rsa;
|
||||
|
||||
public class RsaKeyPolicy : KeyPolicy
|
||||
{
|
||||
public RsaKeyPolicy()
|
||||
{
|
||||
KeyType = KeyType.RSA;
|
||||
KeySize = 2048;
|
||||
}
|
||||
}
|
||||
|
|
@ -4,32 +4,31 @@ using IdentityShroud.Core.Messages;
|
|||
|
||||
namespace IdentityShroud.Core.Security.Keys.Rsa;
|
||||
|
||||
public class RsaKeyPolicy : KeyPolicy
|
||||
{
|
||||
public override string KeyType => "RSA";
|
||||
public int KeySize { get; } = 2048;
|
||||
}
|
||||
|
||||
public class RsaProvider : IKeyProvider
|
||||
{
|
||||
public byte[] CreateKey(KeyPolicy policy)
|
||||
public bool IsPublic => true;
|
||||
|
||||
public KeyData CreateKey(KeyPolicy policy)
|
||||
{
|
||||
if (policy is RsaKeyPolicy p)
|
||||
{
|
||||
using var rsa = RSA.Create(p.KeySize);
|
||||
return rsa.ExportPkcs8PrivateKey();
|
||||
var publicParamaters = rsa.ExportParameters(includePrivateParameters: false);
|
||||
return new KeyData(
|
||||
rsa.ExportPkcs8PrivateKey(),
|
||||
new()
|
||||
{
|
||||
["e"] = Base64Url.EncodeToString(publicParamaters.Exponent),
|
||||
["n"] = Base64Url.EncodeToString(publicParamaters.Modulus),
|
||||
});
|
||||
}
|
||||
|
||||
throw new ArgumentException("Incorrect policy type", nameof(policy));
|
||||
}
|
||||
|
||||
public void SetJwkParameters(byte[] key, JsonWebKey jwk)
|
||||
public void SetJwkParameters(Dictionary<string, string> parameters, JsonWebKey jwk)
|
||||
{
|
||||
using var rsa = RSA.Create();
|
||||
rsa.ImportPkcs8PrivateKey(key, out _);
|
||||
var parameters = rsa.ExportParameters(includePrivateParameters: false);
|
||||
|
||||
jwk.Exponent = Base64Url.EncodeToString(parameters.Exponent);
|
||||
jwk.Modulus = Base64Url.EncodeToString(parameters.Modulus);
|
||||
jwk.Exponent = parameters["e"];
|
||||
jwk.Modulus = parameters["n"];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
using System.Security.Cryptography;
|
||||
using FluentValidation;
|
||||
using IdentityShroud.Core.Contracts;
|
||||
using IdentityShroud.Core.EFCore;
|
||||
using IdentityShroud.Core.Model;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
|
|
@ -8,24 +10,35 @@ namespace IdentityShroud.Core.Services;
|
|||
public class ClientService(
|
||||
Db db,
|
||||
IDataEncryptionService cryptor,
|
||||
IValidator<ClientCreateRequest> clientCreateValidator,
|
||||
IClock clock) : IClientService
|
||||
{
|
||||
public async Task<Result<Client>> Create(Guid realmId, ClientCreateRequest request, CancellationToken ct = default)
|
||||
{
|
||||
clientCreateValidator.ValidateAndThrow(request);
|
||||
|
||||
Realm realm = await db.Realms.FirstOrDefaultAsync(e => e.Id == realmId, ct)
|
||||
?? throw new InvalidOperationException("Require the id of an existing realm");
|
||||
|
||||
Client client = new()
|
||||
{
|
||||
RealmId = realmId,
|
||||
ClientId = request.ClientId,
|
||||
Name = request.Name,
|
||||
Description = request.Description,
|
||||
SignatureAlgorithm = request.SignatureAlgorithm,
|
||||
AllowClientCredentialsFlow = request.AllowClientCredentialsFlow ?? false,
|
||||
SignatureAlgorithm = request.SignatureAlgorithm is null ? null : new(request.SignatureAlgorithm),
|
||||
Confidential = request.Confidential,
|
||||
AllowClientCredentialsFlow = request.AllowClientCredentialsFlow,
|
||||
CreatedAt = clock.UtcNow(),
|
||||
};
|
||||
|
||||
if (client.AllowClientCredentialsFlow)
|
||||
if (request.GenerateSecret is true)
|
||||
{
|
||||
client.Secrets.Add(CreateSecret());
|
||||
await db.Entry(realm).Collection(r => r.DataEncryptionKeys)
|
||||
.Query()
|
||||
.LoadAsync(ct);
|
||||
|
||||
client.Secrets.Add(CreateSecret(realm));
|
||||
}
|
||||
|
||||
await db.AddAsync(client, ct);
|
||||
|
|
@ -50,15 +63,17 @@ public class ClientService(
|
|||
return await db.Clients.FirstOrDefaultAsync(c => c.Id == id && c.RealmId == realmId, ct);
|
||||
}
|
||||
|
||||
private ClientSecret CreateSecret()
|
||||
private ClientSecret CreateSecret(Realm realm)
|
||||
{
|
||||
Span<byte> secret = stackalloc byte[24];
|
||||
RandomNumberGenerator.Fill(secret);
|
||||
|
||||
var dek = realm.DataEncryptionKeys.Single(k => k.Active);
|
||||
|
||||
return new ClientSecret()
|
||||
{
|
||||
CreatedAt = clock.UtcNow(),
|
||||
Secret = cryptor.Encrypt(secret.ToArray()),
|
||||
Secret = cryptor.Encrypt(dek, secret),
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,37 +5,23 @@ using IdentityShroud.Core.Security;
|
|||
namespace IdentityShroud.Core.Services;
|
||||
|
||||
public class DataEncryptionService(
|
||||
IRealmContext realmContext,
|
||||
IDekEncryptionService dekCryptor) : IDataEncryptionService
|
||||
{
|
||||
|
||||
// Note this array is expected to have one item in it most of the during key rotation it will have two
|
||||
// until it is ensured the old key can safely be removed. More then two will work but is not really expected.
|
||||
private IList<RealmDek>? _deks = null;
|
||||
|
||||
private IList<RealmDek> GetDeks()
|
||||
public EncryptedValue Encrypt(RealmDek dek, ReadOnlySpan<byte> plain)
|
||||
{
|
||||
if (_deks is null)
|
||||
_deks = realmContext.GetDeks().Result;
|
||||
|
||||
return _deks;
|
||||
}
|
||||
|
||||
private RealmDek GetActiveDek() => GetDeks().Single(d => d.Active);
|
||||
private RealmDek GetKey(DekId id) => GetDeks().Single(d => d.Id == id);
|
||||
|
||||
public byte[] Decrypt(EncryptedValue input)
|
||||
{
|
||||
var dek = GetKey(input.DekId);
|
||||
var key = dekCryptor.Decrypt(dek.KeyData);
|
||||
return Encryption.Decrypt(input.Value, key);
|
||||
}
|
||||
|
||||
public EncryptedValue Encrypt(ReadOnlySpan<byte> plain)
|
||||
{
|
||||
var dek = GetActiveDek();
|
||||
var key = dekCryptor.Decrypt(dek.KeyData);
|
||||
byte[] cipher = Encryption.Encrypt(plain, key);
|
||||
return new (dek.Id, cipher);
|
||||
}
|
||||
|
||||
public byte[] Decrypt(IReadOnlyList<RealmDek> deks, EncryptedValue input)
|
||||
{
|
||||
// Note a missing key SHOULD not happen. If it does happen something has seriously gone wrong like
|
||||
// - Old key removed before migration completed (should not be possible)
|
||||
// - Wrong keyset because of programming error.
|
||||
var dek = deks.SingleOrDefault(d => d.Id == input.DekId)
|
||||
?? throw new InvalidOperationException("Required key not found");
|
||||
var key = dekCryptor.Decrypt(dek.KeyData);
|
||||
return Encryption.Decrypt(input.Value, key);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,46 +1,16 @@
|
|||
using IdentityShroud.Core.Contracts;
|
||||
using IdentityShroud.Core.Messages;
|
||||
using IdentityShroud.Core.Model;
|
||||
using IdentityShroud.Core.Security.Keys;
|
||||
|
||||
namespace IdentityShroud.Core.Services;
|
||||
|
||||
public class KeyService(
|
||||
IDekEncryptionService cryptor,
|
||||
IKeyProviderFactory keyProviderFactory,
|
||||
IClock clock) : IKeyService
|
||||
IKeyProviderFactory keyProviderFactory) : IKeyService
|
||||
{
|
||||
public RealmKey CreateKey(KeyPolicy policy)
|
||||
public CreateKeyResponse CreateKey(KeyPolicy policy)
|
||||
{
|
||||
IKeyProvider provider = keyProviderFactory.CreateProvider(policy.KeyType);
|
||||
var plainKey = provider.CreateKey(policy);
|
||||
KeyData plainKey = provider.CreateKey(policy);
|
||||
|
||||
return CreateKey(policy.KeyType, plainKey);
|
||||
return new CreateKeyResponse(policy.KeyType, plainKey);
|
||||
}
|
||||
|
||||
public JsonWebKey? CreateJsonWebKey(RealmKey realmKey)
|
||||
{
|
||||
JsonWebKey jwk = new()
|
||||
{
|
||||
KeyId = realmKey.Id.ToString(),
|
||||
KeyType = realmKey.KeyType,
|
||||
Use = "sig",
|
||||
};
|
||||
|
||||
IKeyProvider provider = keyProviderFactory.CreateProvider(realmKey.KeyType);
|
||||
provider.SetJwkParameters(
|
||||
cryptor.Decrypt(realmKey.Key),
|
||||
jwk);
|
||||
|
||||
return jwk;
|
||||
}
|
||||
|
||||
private RealmKey CreateKey(string keyType, byte[] plainKey) =>
|
||||
new RealmKey()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
KeyType = keyType,
|
||||
Key = cryptor.Encrypt(plainKey),
|
||||
CreatedAt = clock.UtcNow(),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
30
IdentityShroud.Core/Services/OpenId/TokenService.cs
Normal file
30
IdentityShroud.Core/Services/OpenId/TokenService.cs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
namespace IdentityShroud.Core.Services.OpenId;
|
||||
|
||||
public interface ITokenService
|
||||
{
|
||||
Task<Result<TokenResponse>> Handle(
|
||||
Dictionary<string, string> form,
|
||||
string? basicAuthUser,
|
||||
string? basicAuthPassword,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
|
||||
public class TokenService : ITokenService
|
||||
{
|
||||
public async Task<Result<TokenResponse>> Handle(
|
||||
Dictionary<string, string> form,
|
||||
string? basicAuthUser,
|
||||
string? basicAuthPassword,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return new();
|
||||
}
|
||||
|
||||
public async Task<Result<TokenResponse>> ClientCredentialsFlow(
|
||||
string clientId,
|
||||
string clientSecret,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return new();
|
||||
}
|
||||
}
|
||||
|
|
@ -16,11 +16,11 @@ public class RealmContext(
|
|||
public async Task<IList<RealmDek>> GetDeks(CancellationToken ct = default)
|
||||
{
|
||||
Realm realm = GetRealm();
|
||||
if (realm.Deks.Count == 0)
|
||||
if (realm.DataEncryptionKeys.Count == 0)
|
||||
{
|
||||
await realmService.LoadDeks(realm);
|
||||
}
|
||||
|
||||
return realm.Deks;
|
||||
return realm.DataEncryptionKeys;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +1,21 @@
|
|||
using IdentityShroud.Core.Contracts;
|
||||
using IdentityShroud.Core.EFCore;
|
||||
using IdentityShroud.Core.Helpers;
|
||||
using IdentityShroud.Core.Messages.Realm;
|
||||
using IdentityShroud.Core.Model;
|
||||
using IdentityShroud.Core.Security;
|
||||
using IdentityShroud.Core.Security.Keys;
|
||||
using IdentityShroud.Core.Security.Keys.Aes;
|
||||
using IdentityShroud.Core.Security.Keys.Rsa;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace IdentityShroud.Core.Services;
|
||||
|
||||
public record RealmCreateResponse(Guid Id, string Slug, string Name);
|
||||
|
||||
public class RealmService(
|
||||
Db db,
|
||||
IKeyService keyService) : IRealmService
|
||||
IKeyService keyService,
|
||||
IDekEncryptionService dekCryptor,
|
||||
IClock clock) : IRealmService
|
||||
{
|
||||
public async Task<Realm?> FindById(Guid id, CancellationToken ct = default)
|
||||
{
|
||||
|
|
@ -26,7 +29,7 @@ public class RealmService(
|
|||
.SingleOrDefaultAsync(r => r.Slug == slug, ct);
|
||||
}
|
||||
|
||||
public async Task<Result<RealmCreateResponse>> Create(RealmCreateRequest request, CancellationToken ct = default)
|
||||
public async Task<Result<Realm>> Create(RealmCreateRequest request, CancellationToken ct = default)
|
||||
{
|
||||
Realm realm = new()
|
||||
{
|
||||
|
|
@ -35,26 +38,52 @@ public class RealmService(
|
|||
Name = request.Name,
|
||||
};
|
||||
|
||||
realm.Keys.Add(keyService.CreateKey(GetKeyPolicy(realm)));
|
||||
realm.TokenSigningKeys.Add(CreateSigningKey(realm));
|
||||
realm.DataEncryptionKeys.Add(CreateDataEncryptionKey(realm));
|
||||
|
||||
db.Add(realm);
|
||||
await db.SaveChangesAsync(ct);
|
||||
|
||||
return new RealmCreateResponse(
|
||||
realm.Id, realm.Slug, realm.Name);
|
||||
|
||||
return realm;
|
||||
}
|
||||
|
||||
private RealmSigningKey CreateSigningKey(Realm realm)
|
||||
{
|
||||
var k = keyService.CreateKey(GetSigningKeyPolicy(realm));
|
||||
return new RealmSigningKey
|
||||
{
|
||||
Id = RealmSigningKeyId.NewId(),
|
||||
KeyType = k.KeyType,
|
||||
Key = dekCryptor.Encrypt(k.Key.PrivateKey),
|
||||
PublicKeyParameters = k.Key.PublicKeyParameters,
|
||||
CreatedAt = clock.UtcNow(),
|
||||
};
|
||||
}
|
||||
|
||||
private RealmDek CreateDataEncryptionKey(Realm realm)
|
||||
{
|
||||
var k = keyService.CreateKey(GetDataKeyPolicy(realm));
|
||||
return new RealmDek()
|
||||
{
|
||||
Id = DekId.NewId(),
|
||||
Active = true,
|
||||
Algorithm = k.KeyType,
|
||||
KeyData = dekCryptor.Encrypt(k.Key.PrivateKey),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Place holder for getting policies from the realm and falling back to sane defaults when no policies have been set.
|
||||
/// </summary>
|
||||
/// <param name="_"></param>
|
||||
/// <returns></returns>
|
||||
private KeyPolicy GetKeyPolicy(Realm _) => new RsaKeyPolicy();
|
||||
|
||||
private KeyPolicy GetSigningKeyPolicy(Realm _) => new RsaKeyPolicy();
|
||||
private KeyPolicy GetDataKeyPolicy(Realm _) => new AesKeyPolicy();
|
||||
|
||||
public async Task LoadActiveKeys(Realm realm)
|
||||
{
|
||||
await db.Entry(realm).Collection(r => r.Keys)
|
||||
await db.Entry(realm).Collection(r => r.TokenSigningKeys)
|
||||
.Query()
|
||||
.Where(k => k.RevokedAt == null)
|
||||
.LoadAsync();
|
||||
|
|
@ -62,7 +91,7 @@ public class RealmService(
|
|||
|
||||
public async Task LoadDeks(Realm realm)
|
||||
{
|
||||
await db.Entry(realm).Collection(r => r.Deks)
|
||||
await db.Entry(realm).Collection(r => r.DataEncryptionKeys)
|
||||
.Query()
|
||||
.LoadAsync();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue