WIP making ClientCreate endpoint
This commit is contained in:
parent
138f335af0
commit
eb872a4f44
28 changed files with 365 additions and 121 deletions
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.Model;
|
||||
using IdentityShroud.Core.Services;
|
||||
|
||||
namespace IdentityShroud.Core.Services;
|
||||
namespace IdentityShroud.Core.Contracts;
|
||||
|
||||
public interface IRealmService
|
||||
{
|
||||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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; } = [];
|
||||
}
|
||||
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 List<Client> Clients { get; init; } = [];
|
||||
|
||||
public List<Key> Keys { get; init; } = [];
|
||||
public List<RealmKey> Keys { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 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(
|
||||
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;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue