5-improve-encrypted-storage #6
28 changed files with 365 additions and 121 deletions
|
|
@ -123,16 +123,15 @@ public class RealmApisTests : IClassFixture<ApplicationFactory>
|
||||||
using var rsa = RSA.Create(2048);
|
using var rsa = RSA.Create(2048);
|
||||||
RSAParameters parameters = rsa.ExportParameters(includePrivateParameters: false);
|
RSAParameters parameters = rsa.ExportParameters(includePrivateParameters: false);
|
||||||
|
|
||||||
Key key = new()
|
RealmKey realmKey = new(
|
||||||
{
|
Guid.NewGuid(),
|
||||||
Id = Guid.NewGuid(),
|
"RSA",
|
||||||
CreatedAt = DateTime.UtcNow,
|
encryptionService.Encrypt(rsa.ExportPkcs8PrivateKey()),
|
||||||
};
|
DateTime.UtcNow);
|
||||||
key.SetPrivateKey(encryptionService, rsa.ExportPkcs8PrivateKey());
|
|
||||||
|
|
||||||
await ScopedContextAsync(async db =>
|
await ScopedContextAsync(async db =>
|
||||||
{
|
{
|
||||||
db.Realms.Add(new Realm() { Slug = "foo", Name = "Foo", Keys = [ key ]});
|
db.Realms.Add(new Realm() { Slug = "foo", Name = "Foo", Keys = [ realmKey ]});
|
||||||
await db.SaveChangesAsync(TestContext.Current.CancellationToken);
|
await db.SaveChangesAsync(TestContext.Current.CancellationToken);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -145,7 +144,7 @@ public class RealmApisTests : IClassFixture<ApplicationFactory>
|
||||||
JsonObject? payload = await response.Content.ReadFromJsonAsync<JsonObject>(TestContext.Current.CancellationToken);
|
JsonObject? payload = await response.Content.ReadFromJsonAsync<JsonObject>(TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
Assert.NotNull(payload);
|
Assert.NotNull(payload);
|
||||||
JsonObjectAssert.Equal(key.Id.ToString(), payload, "keys[0].kid");
|
JsonObjectAssert.Equal(realmKey.Id.ToString(), payload, "keys[0].kid");
|
||||||
JsonObjectAssert.Equal(WebEncoders.Base64UrlEncode(parameters.Modulus!), payload, "keys[0].n");
|
JsonObjectAssert.Equal(WebEncoders.Base64UrlEncode(parameters.Modulus!), payload, "keys[0].n");
|
||||||
JsonObjectAssert.Equal(WebEncoders.Base64UrlEncode(parameters.Exponent!), payload, "keys[0].e");
|
JsonObjectAssert.Equal(WebEncoders.Base64UrlEncode(parameters.Exponent!), payload, "keys[0].e");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,20 +20,20 @@ public class KeyMapperTests
|
||||||
|
|
||||||
RSAParameters parameters = rsa.ExportParameters(includePrivateParameters: false);
|
RSAParameters parameters = rsa.ExportParameters(includePrivateParameters: false);
|
||||||
|
|
||||||
Key key = new()
|
RealmKey realmKey = new()
|
||||||
{
|
{
|
||||||
Id = new("60bb79cf-4bac-4521-87f2-ac87cc15541f"),
|
Id = new("60bb79cf-4bac-4521-87f2-ac87cc15541f"),
|
||||||
CreatedAt = DateTime.UtcNow,
|
CreatedAt = DateTime.UtcNow,
|
||||||
Priority = 10,
|
Priority = 10,
|
||||||
};
|
};
|
||||||
key.SetPrivateKey(_encryptionService, rsa.ExportPkcs8PrivateKey());
|
realmKey.SetPrivateKey(_encryptionService, rsa.ExportPkcs8PrivateKey());
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
KeyMapper mapper = new(_encryptionService);
|
KeyMapper mapper = new(_encryptionService);
|
||||||
JsonWebKey jwk = mapper.KeyToJsonWebKey(key);
|
JsonWebKey jwk = mapper.KeyToJsonWebKey(realmKey);
|
||||||
|
|
||||||
Assert.Equal("RSA", jwk.KeyType);
|
Assert.Equal("RSA", jwk.KeyType);
|
||||||
Assert.Equal(key.Id.ToString(), jwk.KeyId);
|
Assert.Equal(realmKey.Id.ToString(), jwk.KeyId);
|
||||||
Assert.Equal("sig", jwk.Use);
|
Assert.Equal("sig", jwk.Use);
|
||||||
Assert.Equal(parameters.Exponent, WebEncoders.Base64UrlDecode(jwk.Exponent));
|
Assert.Equal(parameters.Exponent, WebEncoders.Base64UrlDecode(jwk.Exponent));
|
||||||
Assert.Equal(parameters.Modulus, WebEncoders.Base64UrlDecode(jwk.Modulus));
|
Assert.Equal(parameters.Modulus, WebEncoders.Base64UrlDecode(jwk.Modulus));
|
||||||
|
|
|
||||||
55
IdentityShroud.Api/Apis/ClientApi.cs
Normal file
55
IdentityShroud.Api/Apis/ClientApi.cs
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
using FluentResults;
|
||||||
|
using IdentityShroud.Core.Contracts;
|
||||||
|
using IdentityShroud.Core.Messages.Realm;
|
||||||
|
using IdentityShroud.Core.Model;
|
||||||
|
using IdentityShroud.Core.Services;
|
||||||
|
using Microsoft.AspNetCore.Http.HttpResults;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace IdentityShroud.Api;
|
||||||
|
|
||||||
|
|
||||||
|
public record ClientCreateReponse(int Id, string ClientId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The part of the api below realms/{slug}/clients
|
||||||
|
/// </summary>
|
||||||
|
public static class ClientApi
|
||||||
|
{
|
||||||
|
public const string ClientGetRouteName = "ClientGet";
|
||||||
|
|
||||||
|
public static void MapEndpoints(this IEndpointRouteBuilder erp)
|
||||||
|
{
|
||||||
|
erp.MapPost("", ClientCreate)
|
||||||
|
.Validate<ClientCreateRequest>()
|
||||||
|
.WithName("ClientCreate")
|
||||||
|
.Produces(StatusCodes.Status201Created);
|
||||||
|
erp.MapGet("{clientId}", ClientGet)
|
||||||
|
.WithName(ClientGetRouteName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Task ClientGet(HttpContext context)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<Results<Created<ClientCreateReponse>, InternalServerError>>
|
||||||
|
ClientCreate(
|
||||||
|
ClientCreateRequest request,
|
||||||
|
[FromServices] IClientService service,
|
||||||
|
HttpContext context,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Realm realm = context.GetValidatedRealm();
|
||||||
|
Result<Client> result = await service.Create(realm.Id, request, cancellationToken);
|
||||||
|
|
||||||
|
// Should i have two set of paths? one for actual REST and one for openid
|
||||||
|
// openid: auth/realms/{realmSlug}/.well-known/openid-configuration
|
||||||
|
// openid: auth/realms/{realmSlug}/openid-connect/(auth|token|jwks)
|
||||||
|
// api: api/v1/realms/{realmId}/....
|
||||||
|
// api: api/v1/realms/{realmId}/clients/{clientId}
|
||||||
|
|
||||||
|
//return Results.CreatedAtRoute(ClientGetRouteName, [ "realmSlug" = realmId!?])
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
using System.Buffers;
|
||||||
|
using System.Buffers.Text;
|
||||||
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace IdentityShroud.Core.Messages;
|
namespace IdentityShroud.Core.Messages;
|
||||||
|
|
@ -25,17 +28,46 @@ public class JsonWebKey
|
||||||
|
|
||||||
// RSA Public Key Components
|
// RSA Public Key Components
|
||||||
[JsonPropertyName("n")]
|
[JsonPropertyName("n")]
|
||||||
public required string Modulus { get; set; }
|
public string? Modulus { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("e")]
|
[JsonPropertyName("e")]
|
||||||
public required string Exponent { get; set; }
|
public string? Exponent { get; set; }
|
||||||
|
|
||||||
|
// ECdsa
|
||||||
|
public string? Curve { get; set; }
|
||||||
|
[JsonConverter(typeof(Base64UrlConverter))]
|
||||||
|
public byte[]? X { get; set; }
|
||||||
|
[JsonConverter(typeof(Base64UrlConverter))]
|
||||||
|
public byte[]? Y { get; set; }
|
||||||
|
|
||||||
// Optional fields
|
// Optional fields
|
||||||
[JsonPropertyName("x5c")]
|
// [JsonPropertyName("x5c")]
|
||||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
// [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
public List<string>? X509CertificateChain { get; set; }
|
// public List<string>? X509CertificateChain { get; set; }
|
||||||
|
//
|
||||||
[JsonPropertyName("x5t")]
|
// [JsonPropertyName("x5t")]
|
||||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
// [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
public string? X509CertificateThumbprint { get; set; }
|
// public string? X509CertificateThumbprint { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Base64UrlConverter : JsonConverter<byte[]>
|
||||||
|
{
|
||||||
|
public override byte[] Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
// GetValueSpan gives you the raw UTF-8 bytes of the JSON string value
|
||||||
|
if (reader.HasValueSequence)
|
||||||
|
{
|
||||||
|
var valueSequence = reader.ValueSequence.ToArray();
|
||||||
|
return Base64Url.DecodeFromUtf8(valueSequence);
|
||||||
|
}
|
||||||
|
return Base64Url.DecodeFromUtf8(reader.ValueSpan);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(Utf8JsonWriter writer, byte[] value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
int encodedLength = Base64Url.GetEncodedLength(value.Length);
|
||||||
|
Span<byte> buffer = encodedLength <= 256 ? stackalloc byte[encodedLength] : new byte[encodedLength];
|
||||||
|
Base64Url.EncodeToUtf8(value, buffer);
|
||||||
|
writer.WriteStringValue(buffer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using IdentityShroud.Core.Contracts;
|
||||||
using IdentityShroud.Core.Model;
|
using IdentityShroud.Core.Model;
|
||||||
using IdentityShroud.Core.Services;
|
using IdentityShroud.Core.Services;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,26 +9,44 @@ namespace IdentityShroud.Api.Mappers;
|
||||||
|
|
||||||
public class KeyMapper(IEncryptionService encryptionService)
|
public class KeyMapper(IEncryptionService encryptionService)
|
||||||
{
|
{
|
||||||
public JsonWebKey KeyToJsonWebKey(Key key)
|
public JsonWebKey? KeyToJsonWebKey(RealmKey realmKey)
|
||||||
{
|
{
|
||||||
using var rsa = RsaHelper.LoadFromPkcs8(key.GetPrivateKey(encryptionService));
|
|
||||||
RSAParameters parameters = rsa.ExportParameters(includePrivateParameters: false);
|
|
||||||
|
|
||||||
return new JsonWebKey()
|
JsonWebKey result = new()
|
||||||
{
|
{
|
||||||
KeyType = rsa.SignatureAlgorithm,
|
KeyId = realmKey.Id.ToString(),
|
||||||
KeyId = key.Id.ToString(),
|
|
||||||
Use = "sig",
|
Use = "sig",
|
||||||
Exponent = WebEncoders.Base64UrlEncode(parameters.Exponent!),
|
|
||||||
Modulus = WebEncoders.Base64UrlEncode(parameters.Modulus!),
|
|
||||||
};
|
};
|
||||||
|
switch (realmKey.KeyType)
|
||||||
|
{
|
||||||
|
case "RSA":
|
||||||
|
using (var rsa = RsaHelper.LoadFromPkcs8(realmKey.GetPrivateKey(encryptionService)))
|
||||||
|
{
|
||||||
|
RSAParameters parameters = rsa.ExportParameters(includePrivateParameters: false);
|
||||||
|
result.KeyType = rsa.SignatureAlgorithm;
|
||||||
|
result.Exponent = WebEncoders.Base64UrlEncode(parameters.Exponent!);
|
||||||
|
result.Modulus = WebEncoders.Base64UrlEncode(parameters.Modulus!);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public JsonWebKeySet KeyListToJsonWebKeySet(IEnumerable<Key> keys)
|
public JsonWebKeySet KeyListToJsonWebKeySet(IEnumerable<RealmKey> keys)
|
||||||
{
|
{
|
||||||
return new JsonWebKeySet()
|
JsonWebKeySet wks = new();
|
||||||
|
foreach (var k in keys)
|
||||||
{
|
{
|
||||||
Keys = keys.Select(e => KeyToJsonWebKey(e)).ToList(),
|
var wk = KeyToJsonWebKey(k);
|
||||||
};
|
if (wk is {})
|
||||||
|
{
|
||||||
|
wks.Keys.Add(wk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return wks;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
using FluentResults;
|
using FluentResults;
|
||||||
using IdentityShroud.Api.Mappers;
|
using IdentityShroud.Api.Mappers;
|
||||||
using IdentityShroud.Api.Validation;
|
using IdentityShroud.Core.Contracts;
|
||||||
using IdentityShroud.Core.Messages;
|
using IdentityShroud.Core.Messages;
|
||||||
using IdentityShroud.Core.Messages.Realm;
|
using IdentityShroud.Core.Messages.Realm;
|
||||||
using IdentityShroud.Core.Model;
|
using IdentityShroud.Core.Model;
|
||||||
|
|
@ -19,18 +19,21 @@ public static class HttpContextExtensions
|
||||||
|
|
||||||
public static class RealmApi
|
public static class RealmApi
|
||||||
{
|
{
|
||||||
public static void MapRealmEndpoints(this IEndpointRouteBuilder app)
|
public static void MapRealmEndpoints(this IEndpointRouteBuilder erp)
|
||||||
{
|
{
|
||||||
var realmsGroup = app.MapGroup("/realms");
|
var realmsGroup = erp.MapGroup("/realms");
|
||||||
realmsGroup.MapPost("", RealmCreate)
|
realmsGroup.MapPost("", RealmCreate)
|
||||||
.Validate<RealmCreateRequest>()
|
.Validate<RealmCreateRequest>()
|
||||||
.WithName("Create Realm")
|
.WithName("Create Realm")
|
||||||
.Produces(StatusCodes.Status201Created);
|
.Produces(StatusCodes.Status201Created);
|
||||||
|
|
||||||
var realmSlugGroup = realmsGroup.MapGroup("{slug}")
|
var realmSlugGroup = realmsGroup.MapGroup("{realmSlug}")
|
||||||
.AddEndpointFilter<SlugValidationFilter>();
|
.AddEndpointFilter<SlugValidationFilter>();
|
||||||
realmSlugGroup.MapGet(".well-known/openid-configuration", GetOpenIdConfiguration);
|
realmSlugGroup.MapGet(".well-known/openid-configuration", GetOpenIdConfiguration);
|
||||||
|
|
||||||
|
RouteGroupBuilder clientsGroup = realmSlugGroup.MapGroup("clients");
|
||||||
|
|
||||||
|
|
||||||
var openidConnect = realmSlugGroup.MapGroup("openid-connect");
|
var openidConnect = realmSlugGroup.MapGroup("openid-connect");
|
||||||
openidConnect.MapPost("auth", OpenIdConnectAuth);
|
openidConnect.MapPost("auth", OpenIdConnectAuth);
|
||||||
openidConnect.MapPost("token", OpenIdConnectToken);
|
openidConnect.MapPost("token", OpenIdConnectToken);
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=apis_005Cdto/@EntryIndexedValue">True</s:Boolean>
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=apis_005Cdto/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=apis_005Cfilters/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=apis_005Cfilters/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=validation/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
using FluentValidation;
|
using FluentValidation;
|
||||||
using IdentityShroud.Api;
|
using IdentityShroud.Api;
|
||||||
using IdentityShroud.Api.Mappers;
|
using IdentityShroud.Api.Mappers;
|
||||||
using IdentityShroud.Api.Validation;
|
|
||||||
using IdentityShroud.Core;
|
using IdentityShroud.Core;
|
||||||
using IdentityShroud.Core.Contracts;
|
using IdentityShroud.Core.Contracts;
|
||||||
using IdentityShroud.Core.Security;
|
using IdentityShroud.Core.Security;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
namespace IdentityShroud.Api.Validation;
|
namespace IdentityShroud.Api;
|
||||||
|
|
||||||
public static class EndpointRouteBuilderExtensions
|
public static class EndpointRouteBuilderExtensions
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
using FluentValidation;
|
using FluentValidation;
|
||||||
using IdentityShroud.Core.Messages.Realm;
|
using IdentityShroud.Core.Messages.Realm;
|
||||||
|
|
||||||
namespace IdentityShroud.Api.Validation;
|
namespace IdentityShroud.Api;
|
||||||
|
|
||||||
public class RealmCreateRequestValidator : AbstractValidator<RealmCreateRequest>
|
public class RealmCreateRequestValidator : AbstractValidator<RealmCreateRequest>
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
using FluentValidation;
|
using FluentValidation;
|
||||||
|
|
||||||
namespace IdentityShroud.Api.Validation;
|
namespace IdentityShroud.Api;
|
||||||
|
|
||||||
public class ValidateFilter<T> : IEndpointFilter where T : class
|
public class ValidateFilter<T> : IEndpointFilter where T : class
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ using IdentityShroud.Core.Model;
|
||||||
|
|
||||||
namespace IdentityShroud.Core.Tests.Model;
|
namespace IdentityShroud.Core.Tests.Model;
|
||||||
|
|
||||||
public class KeyTests
|
public class RealmKeyTests
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public void SetNewKey()
|
public void SetNewKey()
|
||||||
|
|
@ -16,12 +16,12 @@ public class KeyTests
|
||||||
.Encrypt(Arg.Any<byte[]>())
|
.Encrypt(Arg.Any<byte[]>())
|
||||||
.Returns(x => encryptedPrivateKey);
|
.Returns(x => encryptedPrivateKey);
|
||||||
|
|
||||||
Key key = new();
|
RealmKey realmKey = new();
|
||||||
key.SetPrivateKey(encryptionService, privateKey);
|
realmKey.SetPrivateKey(encryptionService, privateKey);
|
||||||
|
|
||||||
// should be able to return original without calling decrypt
|
// should be able to return original without calling decrypt
|
||||||
Assert.Equal(privateKey, key.GetPrivateKey(encryptionService));
|
Assert.Equal(privateKey, realmKey.GetPrivateKey(encryptionService));
|
||||||
Assert.Equal(encryptedPrivateKey, key.PrivateKeyEncrypted);
|
Assert.Equal(encryptedPrivateKey, realmKey.PrivateKeyEncrypted);
|
||||||
|
|
||||||
encryptionService.Received(1).Encrypt(privateKey);
|
encryptionService.Received(1).Encrypt(privateKey);
|
||||||
encryptionService.DidNotReceive().Decrypt(Arg.Any<byte[]>());
|
encryptionService.DidNotReceive().Decrypt(Arg.Any<byte[]>());
|
||||||
|
|
@ -38,12 +38,12 @@ public class KeyTests
|
||||||
.Decrypt(encryptedPrivateKey)
|
.Decrypt(encryptedPrivateKey)
|
||||||
.Returns(x => privateKey);
|
.Returns(x => privateKey);
|
||||||
|
|
||||||
Key key = new();
|
RealmKey realmKey = new();
|
||||||
key.PrivateKeyEncrypted = encryptedPrivateKey;
|
realmKey.PrivateKeyEncrypted = encryptedPrivateKey;
|
||||||
|
|
||||||
// should be able to return original without calling decrypt
|
// should be able to return original without calling decrypt
|
||||||
Assert.Equal(privateKey, key.GetPrivateKey(encryptionService));
|
Assert.Equal(privateKey, realmKey.GetPrivateKey(encryptionService));
|
||||||
Assert.Equal(encryptedPrivateKey, key.PrivateKeyEncrypted);
|
Assert.Equal(encryptedPrivateKey, realmKey.PrivateKeyEncrypted);
|
||||||
|
|
||||||
encryptionService.Received(1).Decrypt(encryptedPrivateKey);
|
encryptionService.Received(1).Decrypt(encryptedPrivateKey);
|
||||||
}
|
}
|
||||||
25
IdentityShroud.Core/Contracts/IClientService.cs
Normal file
25
IdentityShroud.Core/Contracts/IClientService.cs
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
using IdentityShroud.Core.Model;
|
||||||
|
|
||||||
|
namespace IdentityShroud.Core.Contracts;
|
||||||
|
|
||||||
|
//public record CreateClientRequest(Guid RealmId, string ClientId, string? Description);
|
||||||
|
|
||||||
|
public class ClientCreateRequest
|
||||||
|
{
|
||||||
|
public 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 interface IClientService
|
||||||
|
{
|
||||||
|
Task<Result<Client>> Create(
|
||||||
|
Guid realmId,
|
||||||
|
ClientCreateRequest request,
|
||||||
|
CancellationToken ct = default);
|
||||||
|
|
||||||
|
Task<Client?> GetByClientId(string clientId, CancellationToken ct = default);
|
||||||
|
}
|
||||||
6
IdentityShroud.Core/Contracts/IClock.cs
Normal file
6
IdentityShroud.Core/Contracts/IClock.cs
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
namespace IdentityShroud.Core.Contracts;
|
||||||
|
|
||||||
|
public interface IClock
|
||||||
|
{
|
||||||
|
DateTime UtcNow();
|
||||||
|
}
|
||||||
8
IdentityShroud.Core/Contracts/IKeyProvisioningService.cs
Normal file
8
IdentityShroud.Core/Contracts/IKeyProvisioningService.cs
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
using IdentityShroud.Core.Model;
|
||||||
|
|
||||||
|
namespace IdentityShroud.Core.Contracts;
|
||||||
|
|
||||||
|
public interface IKeyProvisioningService
|
||||||
|
{
|
||||||
|
RealmKey CreateRsaKey(int keySize = 2048);
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
using IdentityShroud.Core.Messages.Realm;
|
using IdentityShroud.Core.Messages.Realm;
|
||||||
using IdentityShroud.Core.Model;
|
using IdentityShroud.Core.Model;
|
||||||
|
using IdentityShroud.Core.Services;
|
||||||
|
|
||||||
namespace IdentityShroud.Core.Services;
|
namespace IdentityShroud.Core.Contracts;
|
||||||
|
|
||||||
public interface IRealmService
|
public interface IRealmService
|
||||||
{
|
{
|
||||||
|
|
@ -16,8 +16,9 @@ public class Db(
|
||||||
ILoggerFactory? loggerFactory)
|
ILoggerFactory? loggerFactory)
|
||||||
: DbContext
|
: DbContext
|
||||||
{
|
{
|
||||||
|
public virtual DbSet<Client> Clients { get; set; }
|
||||||
public virtual DbSet<Realm> Realms { get; set; }
|
public virtual DbSet<Realm> Realms { get; set; }
|
||||||
public virtual DbSet<Key> Keys { get; set; }
|
public virtual DbSet<RealmKey> Keys { get; set; }
|
||||||
|
|
||||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,30 @@
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
using IdentityShroud.Core.Security;
|
using IdentityShroud.Core.Security;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace IdentityShroud.Core.Model;
|
namespace IdentityShroud.Core.Model;
|
||||||
|
|
||||||
|
[Table("client")]
|
||||||
|
[Index(nameof(ClientId), IsUnique = true)]
|
||||||
public class Client
|
public class Client
|
||||||
{
|
{
|
||||||
|
[Key]
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
public string Name { get; set; }
|
public Guid RealmId { get; set; }
|
||||||
|
[MaxLength(40)]
|
||||||
|
public required string ClientId { get; set; }
|
||||||
|
[MaxLength(80)]
|
||||||
|
public string? Name { get; set; }
|
||||||
|
[MaxLength(2048)]
|
||||||
|
public string? Description { get; set; }
|
||||||
|
|
||||||
public string? SignatureAlgorithm { get; set; } = JsonWebAlgorithm.RS256;
|
[MaxLength(20)]
|
||||||
|
public string? SignatureAlgorithm { get; set; }
|
||||||
|
|
||||||
|
public bool AllowClientCredentialsFlow { get; set; } = false;
|
||||||
|
|
||||||
|
public required DateTime CreatedAt { get; set; }
|
||||||
|
|
||||||
|
public List<ClientSecret> Secrets { get; set; } = [];
|
||||||
}
|
}
|
||||||
15
IdentityShroud.Core/Model/ClientSecret.cs
Normal file
15
IdentityShroud.Core/Model/ClientSecret.cs
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace IdentityShroud.Core.Model;
|
||||||
|
|
||||||
|
[Table("client_secret")]
|
||||||
|
public class ClientSecret
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
public int Id { get; set; }
|
||||||
|
public Guid ClientId { get; set; }
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public DateTime? RevokedAt { get; set; }
|
||||||
|
public required byte[] SecretEncrypted { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
using IdentityShroud.Core.Contracts;
|
|
||||||
|
|
||||||
namespace IdentityShroud.Core.Model;
|
|
||||||
|
|
||||||
|
|
||||||
[Table("key")]
|
|
||||||
public class Key
|
|
||||||
{
|
|
||||||
private byte[] _privateKeyDecrypted = [];
|
|
||||||
|
|
||||||
public Guid Id { get; set; }
|
|
||||||
|
|
||||||
public DateTime CreatedAt { get; set; }
|
|
||||||
public DateTime? DeactivatedAt { 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 byte[] PrivateKeyEncrypted
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
field = value;
|
|
||||||
_privateKeyDecrypted = [];
|
|
||||||
}
|
|
||||||
} = [];
|
|
||||||
|
|
||||||
public byte[] GetPrivateKey(IEncryptionService encryptionService)
|
|
||||||
{
|
|
||||||
if (_privateKeyDecrypted.Length == 0 && PrivateKeyEncrypted.Length > 0)
|
|
||||||
_privateKeyDecrypted = encryptionService.Decrypt(PrivateKeyEncrypted);
|
|
||||||
return _privateKeyDecrypted;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetPrivateKey(IEncryptionService encryptionService, byte[] privateKey)
|
|
||||||
{
|
|
||||||
PrivateKeyEncrypted = encryptionService.Encrypt(privateKey);
|
|
||||||
_privateKeyDecrypted = privateKey;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -20,7 +20,7 @@ public class Realm
|
||||||
public string Name { get; set; } = "";
|
public string Name { get; set; } = "";
|
||||||
public List<Client> Clients { get; init; } = [];
|
public List<Client> Clients { get; init; } = [];
|
||||||
|
|
||||||
public List<Key> Keys { get; init; } = [];
|
public List<RealmKey> Keys { get; init; } = [];
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Can be overriden per client
|
/// Can be overriden per client
|
||||||
|
|
|
||||||
22
IdentityShroud.Core/Model/RealmKey.cs
Normal file
22
IdentityShroud.Core/Model/RealmKey.cs
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace IdentityShroud.Core.Model;
|
||||||
|
|
||||||
|
|
||||||
|
[Table("realm_key")]
|
||||||
|
public record RealmKey(Guid Id, string KeyType, byte[] KeyDataEncrypted, DateTime CreatedAt)
|
||||||
|
{
|
||||||
|
public Guid Id { get; private set; } = Id;
|
||||||
|
public string KeyType { get; private set; } = KeyType;
|
||||||
|
public byte[] KeyDataEncrypted { get; private set; } = KeyDataEncrypted;
|
||||||
|
public DateTime CreatedAt { get; private set; } = CreatedAt;
|
||||||
|
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;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
54
IdentityShroud.Core/Services/ClientService.cs
Normal file
54
IdentityShroud.Core/Services/ClientService.cs
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using IdentityShroud.Core.Contracts;
|
||||||
|
using IdentityShroud.Core.Model;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace IdentityShroud.Core.Services;
|
||||||
|
|
||||||
|
public class ClientService(
|
||||||
|
Db db,
|
||||||
|
IEncryptionService cryptor,
|
||||||
|
IClock clock) : IClientService
|
||||||
|
{
|
||||||
|
public async Task<Result<Client>> Create(Guid realmId, ClientCreateRequest request, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
Client client = new()
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
RealmId = realmId,
|
||||||
|
ClientId = request.ClientId,
|
||||||
|
Name = request.Name,
|
||||||
|
Description = request.Description,
|
||||||
|
SignatureAlgorithm = request.SignatureAlgorithm,
|
||||||
|
AllowClientCredentialsFlow = request.AllowClientCredentialsFlow ?? false,
|
||||||
|
CreatedAt = clock.UtcNow(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (client.AllowClientCredentialsFlow)
|
||||||
|
{
|
||||||
|
client.Secrets.Add(CreateSecret());
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.AddAsync(client, ct);
|
||||||
|
await db.SaveChangesAsync(ct);
|
||||||
|
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Client?> GetByClientId(string clientId, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await db.Clients.FirstOrDefaultAsync(c => c.ClientId == clientId, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ClientSecret CreateSecret()
|
||||||
|
{
|
||||||
|
byte[] secret = RandomNumberGenerator.GetBytes(24);
|
||||||
|
|
||||||
|
return new ClientSecret()
|
||||||
|
{
|
||||||
|
CreatedAt = clock.UtcNow(),
|
||||||
|
SecretEncrypted = cryptor.Encrypt(secret),
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
11
IdentityShroud.Core/Services/ClockService.cs
Normal file
11
IdentityShroud.Core/Services/ClockService.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
using IdentityShroud.Core.Contracts;
|
||||||
|
|
||||||
|
namespace IdentityShroud.Core.Services;
|
||||||
|
|
||||||
|
public class ClockService : IClock
|
||||||
|
{
|
||||||
|
public DateTime UtcNow()
|
||||||
|
{
|
||||||
|
return DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
}
|
||||||
30
IdentityShroud.Core/Services/KeyProvisioningService.cs
Normal file
30
IdentityShroud.Core/Services/KeyProvisioningService.cs
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using IdentityShroud.Core.Contracts;
|
||||||
|
using IdentityShroud.Core.Model;
|
||||||
|
|
||||||
|
namespace IdentityShroud.Core.Services;
|
||||||
|
|
||||||
|
public class KeyProvisioningService(
|
||||||
|
IEncryptionService encryptionService,
|
||||||
|
IClock clock) : IKeyProvisioningService
|
||||||
|
{
|
||||||
|
public RealmKey CreateRsaKey(int keySize = 2048)
|
||||||
|
{
|
||||||
|
using var rsa = RSA.Create(keySize);
|
||||||
|
return CreateKey("RSA", rsa.ExportPkcs8PrivateKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
private RealmKey CreateKey(string keyType, byte[] keyData) =>
|
||||||
|
new RealmKey(
|
||||||
|
Guid.NewGuid(),
|
||||||
|
keyType,
|
||||||
|
encryptionService.Encrypt(keyData),
|
||||||
|
clock.UtcNow());
|
||||||
|
|
||||||
|
// public byte[] GetPrivateKey(IEncryptionService encryptionService)
|
||||||
|
// {
|
||||||
|
// if (_privateKeyDecrypted.Length == 0 && PrivateKeyEncrypted.Length > 0)
|
||||||
|
// _privateKeyDecrypted = encryptionService.Decrypt(PrivateKeyEncrypted);
|
||||||
|
// return _privateKeyDecrypted;
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
@ -11,7 +11,7 @@ public record RealmCreateResponse(Guid Id, string Slug, string Name);
|
||||||
|
|
||||||
public class RealmService(
|
public class RealmService(
|
||||||
Db db,
|
Db db,
|
||||||
IEncryptionService encryptionService) : IRealmService
|
IKeyProvisioningService keyProvisioningService) : IRealmService
|
||||||
{
|
{
|
||||||
public async Task<Realm?> FindBySlug(string slug, CancellationToken ct = default)
|
public async Task<Realm?> FindBySlug(string slug, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
|
|
@ -26,7 +26,7 @@ public class RealmService(
|
||||||
Id = request.Id ?? Guid.CreateVersion7(),
|
Id = request.Id ?? Guid.CreateVersion7(),
|
||||||
Slug = request.Slug ?? SlugHelper.GenerateSlug(request.Name),
|
Slug = request.Slug ?? SlugHelper.GenerateSlug(request.Name),
|
||||||
Name = request.Name,
|
Name = request.Name,
|
||||||
Keys = [ CreateKey() ],
|
Keys = [ keyProvisioningService.CreateRsaKey() ],
|
||||||
};
|
};
|
||||||
|
|
||||||
db.Add(realm);
|
db.Add(realm);
|
||||||
|
|
@ -40,21 +40,8 @@ public class RealmService(
|
||||||
{
|
{
|
||||||
await db.Entry(realm).Collection(r => r.Keys)
|
await db.Entry(realm).Collection(r => r.Keys)
|
||||||
.Query()
|
.Query()
|
||||||
.Where(k => k.DeactivatedAt == null)
|
.Where(k => k.RevokedAt == null)
|
||||||
.LoadAsync();
|
.LoadAsync();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Key CreateKey()
|
|
||||||
{
|
|
||||||
using RSA rsa = RSA.Create(2048);
|
|
||||||
|
|
||||||
Key key = new()
|
|
||||||
{
|
|
||||||
Priority = 10,
|
|
||||||
};
|
|
||||||
key.SetPrivateKey(encryptionService, rsa.ExportPkcs8PrivateKey());
|
|
||||||
|
|
||||||
return key;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -13,11 +13,13 @@
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APostgreSqlBuilder_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fcdd0beaf7beaf8366c0862f34fe40da30911084d957625ab31577851ee8cae7_003FPostgreSqlBuilder_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APostgreSqlBuilder_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fcdd0beaf7beaf8366c0862f34fe40da30911084d957625ab31577851ee8cae7_003FPostgreSqlBuilder_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APostgreSqlContainer_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fc82112acf224de1d157da0309437b227be6c1ef877865c23872f49eaf9d73c_003FPostgreSqlContainer_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APostgreSqlContainer_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fc82112acf224de1d157da0309437b227be6c1ef877865c23872f49eaf9d73c_003FPostgreSqlContainer_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AResultsOfT_002EGenerated_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fff2e2c5ca93c7786ef8425ca6caf751702328924211687ce72e74fd1265e8_003FResultsOfT_002EGenerated_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AResultsOfT_002EGenerated_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fff2e2c5ca93c7786ef8425ca6caf751702328924211687ce72e74fd1265e8_003FResultsOfT_002EGenerated_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ARouteGroupBuilder_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fd42b8f8feda3bfb3dc17f133a52ce45931ed5066c46a4d834c8ed46e0a6_003FRouteGroupBuilder_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ATypedResults_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fcea118513a410f660e578fe32bed95cf86457dd135e4b4632ca91eb4f7b_003FTypedResults_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ATypedResults_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fcea118513a410f660e578fe32bed95cf86457dd135e4b4632ca91eb4f7b_003FTypedResults_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AWebEncoders_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fce6b69dd397f614758bc5821136ec8af3fa22563dd657769e231f51be1fbbc_003FWebEncoders_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/dotCover/Editor/HighlightingSourceSnapshotLocation/@EntryValue">/home/eelke/.cache/JetBrains/Rider2025.3/resharper-host/temp/Rider/vAny/CoverageData/_IdentityShroud.-1277985570/Snapshot/snapshot.utdcvr</s:String>
|
<s:String x:Key="/Default/dotCover/Editor/HighlightingSourceSnapshotLocation/@EntryValue">/home/eelke/.cache/JetBrains/Rider2025.3/resharper-host/temp/Rider/vAny/CoverageData/_IdentityShroud.-1277985570/Snapshot/snapshot.utdcvr</s:String>
|
||||||
<s:String x:Key="/Default/Environment/Hierarchy/Build/BuildTool/DotNetCliExePath/@EntryValue">/home/eelke/.dotnet/dotnet</s:String>
|
<s:String x:Key="/Default/Environment/Hierarchy/Build/BuildTool/DotNetCliExePath/@EntryValue">/home/eelke/.dotnet/dotnet</s:String>
|
||||||
<s:String x:Key="/Default/Environment/Hierarchy/Build/BuildTool/CustomBuildToolPath/@EntryValue">/home/eelke/.dotnet/sdk/10.0.102/MSBuild.dll</s:String>
|
<s:String x:Key="/Default/Environment/Hierarchy/Build/BuildTool/CustomBuildToolPath/@EntryValue">/home/eelke/.dotnet/sdk/10.0.102/MSBuild.dll</s:String>
|
||||||
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=6e5d049f_002D5af8_002D43d4_002D878d_002D591b09b1e74a/@EntryIndexedValue"><SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from Solution #3" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
|
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=6e5d049f_002D5af8_002D43d4_002D878d_002D591b09b1e74a/@EntryIndexedValue"><SessionState ContinuousTestingMode="0" Name="All tests from Solution #3" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
|
||||||
<Solution />
|
<Solution />
|
||||||
</SessionState></s:String>
|
</SessionState></s:String>
|
||||||
|
|
||||||
|
|
@ -25,7 +27,7 @@
|
||||||
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=a4b5fea0_002D4511_002D4f66_002D888d_002Daea8a1e4c94d/@EntryIndexedValue"><SessionState ContinuousTestingMode="0" Name="All tests from Solution" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
|
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=a4b5fea0_002D4511_002D4f66_002D888d_002Daea8a1e4c94d/@EntryIndexedValue"><SessionState ContinuousTestingMode="0" Name="All tests from Solution" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
|
||||||
<Solution />
|
<Solution />
|
||||||
</SessionState></s:String>
|
</SessionState></s:String>
|
||||||
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=b6b17914_002D7f7b_002D403e_002Db1eb_002D2c847c515018/@EntryIndexedValue"><SessionState ContinuousTestingMode="0" Name="All tests from Solution #2" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
|
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=b6b17914_002D7f7b_002D403e_002Db1eb_002D2c847c515018/@EntryIndexedValue"><SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from Solution #2" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
|
||||||
<Solution />
|
<Solution />
|
||||||
</SessionState></s:String>
|
</SessionState></s:String>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue