WIP making ClientCreate endpoint

This commit is contained in:
eelke 2026-02-20 17:35:38 +01:00
parent 138f335af0
commit eb872a4f44
28 changed files with 365 additions and 121 deletions

View 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);
}

View file

@ -0,0 +1,6 @@
namespace IdentityShroud.Core.Contracts;
public interface IClock
{
DateTime UtcNow();
}

View file

@ -0,0 +1,8 @@
using IdentityShroud.Core.Model;
namespace IdentityShroud.Core.Contracts;
public interface IKeyProvisioningService
{
RealmKey CreateRsaKey(int keySize = 2048);
}

View file

@ -1,7 +1,8 @@
using IdentityShroud.Core.Messages.Realm;
using IdentityShroud.Core.Model;
using IdentityShroud.Core.Services;
namespace IdentityShroud.Core.Services;
namespace IdentityShroud.Core.Contracts;
public interface IRealmService
{

View file

@ -16,8 +16,9 @@ public class Db(
ILoggerFactory? loggerFactory)
: DbContext
{
public virtual DbSet<Client> Clients { 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)
{

View file

@ -1,11 +1,30 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using IdentityShroud.Core.Security;
using Microsoft.EntityFrameworkCore;
namespace IdentityShroud.Core.Model;
[Table("client")]
[Index(nameof(ClientId), IsUnique = true)]
public class Client
{
[Key]
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; } = [];
}

View 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; }
}

View file

@ -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;
}
}

View file

@ -20,7 +20,7 @@ public class Realm
public string Name { get; set; } = "";
public List<Client> Clients { get; init; } = [];
public List<Key> Keys { get; init; } = [];
public List<RealmKey> Keys { get; init; } = [];
/// <summary>
/// Can be overriden per client

View 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;
}

View 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),
};
}
}

View file

@ -0,0 +1,11 @@
using IdentityShroud.Core.Contracts;
namespace IdentityShroud.Core.Services;
public class ClockService : IClock
{
public DateTime UtcNow()
{
return DateTime.UtcNow;
}
}

View 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;
// }
}

View file

@ -11,7 +11,7 @@ public record RealmCreateResponse(Guid Id, string Slug, string Name);
public class RealmService(
Db db,
IEncryptionService encryptionService) : IRealmService
IKeyProvisioningService keyProvisioningService) : IRealmService
{
public async Task<Realm?> FindBySlug(string slug, CancellationToken ct = default)
{
@ -26,7 +26,7 @@ public class RealmService(
Id = request.Id ?? Guid.CreateVersion7(),
Slug = request.Slug ?? SlugHelper.GenerateSlug(request.Name),
Name = request.Name,
Keys = [ CreateKey() ],
Keys = [ keyProvisioningService.CreateRsaKey() ],
};
db.Add(realm);
@ -40,21 +40,8 @@ public class RealmService(
{
await db.Entry(realm).Collection(r => r.Keys)
.Query()
.Where(k => k.DeactivatedAt == null)
.Where(k => k.RevokedAt == null)
.LoadAsync();
}
private Key CreateKey()
{
using RSA rsa = RSA.Create(2048);
Key key = new()
{
Priority = 10,
};
key.SetPrivateKey(encryptionService, rsa.ExportPkcs8PrivateKey());
return key;
}
}