2026-02-27 17:57:42 +00:00
|
|
|
using System.Security.Cryptography;
|
2026-03-16 19:15:04 +01:00
|
|
|
using FluentValidation;
|
2026-02-27 17:57:42 +00:00
|
|
|
using IdentityShroud.Core.Contracts;
|
2026-03-16 19:15:04 +01:00
|
|
|
using IdentityShroud.Core.EFCore;
|
2026-02-27 17:57:42 +00:00
|
|
|
using IdentityShroud.Core.Model;
|
|
|
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
|
|
|
|
|
|
namespace IdentityShroud.Core.Services;
|
|
|
|
|
|
|
|
|
|
public class ClientService(
|
|
|
|
|
Db db,
|
|
|
|
|
IDataEncryptionService cryptor,
|
2026-03-16 19:15:04 +01:00
|
|
|
IValidator<ClientCreateRequest> clientCreateValidator,
|
2026-02-27 17:57:42 +00:00
|
|
|
IClock clock) : IClientService
|
|
|
|
|
{
|
|
|
|
|
public async Task<Result<Client>> Create(Guid realmId, ClientCreateRequest request, CancellationToken ct = default)
|
|
|
|
|
{
|
2026-03-16 19:15:04 +01:00
|
|
|
clientCreateValidator.ValidateAndThrow(request);
|
|
|
|
|
|
|
|
|
|
Realm realm = await db.Realms.FirstOrDefaultAsync(e => e.Id == realmId, ct)
|
|
|
|
|
?? throw new InvalidOperationException("Require the id of an existing realm");
|
|
|
|
|
|
2026-02-27 17:57:42 +00:00
|
|
|
Client client = new()
|
|
|
|
|
{
|
|
|
|
|
RealmId = realmId,
|
|
|
|
|
ClientId = request.ClientId,
|
|
|
|
|
Name = request.Name,
|
|
|
|
|
Description = request.Description,
|
2026-03-16 19:15:04 +01:00
|
|
|
SignatureAlgorithm = request.SignatureAlgorithm is null ? null : new(request.SignatureAlgorithm),
|
|
|
|
|
Confidential = request.Confidential,
|
|
|
|
|
AllowClientCredentialsFlow = request.AllowClientCredentialsFlow,
|
2026-02-27 17:57:42 +00:00
|
|
|
CreatedAt = clock.UtcNow(),
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-16 19:15:04 +01:00
|
|
|
if (request.GenerateSecret is true)
|
2026-02-27 17:57:42 +00:00
|
|
|
{
|
2026-03-16 19:15:04 +01:00
|
|
|
await db.Entry(realm).Collection(r => r.DataEncryptionKeys)
|
|
|
|
|
.Query()
|
|
|
|
|
.LoadAsync(ct);
|
|
|
|
|
|
|
|
|
|
client.Secrets.Add(CreateSecret(realm));
|
2026-02-27 17:57:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await db.AddAsync(client, ct);
|
|
|
|
|
await db.SaveChangesAsync(ct);
|
|
|
|
|
|
|
|
|
|
return client;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<Client?> GetByClientId(
|
|
|
|
|
Guid realmId,
|
|
|
|
|
string clientId,
|
|
|
|
|
CancellationToken ct = default)
|
|
|
|
|
{
|
|
|
|
|
return await db.Clients.FirstOrDefaultAsync(c => c.ClientId == clientId && c.RealmId == realmId, ct);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<Client?> FindById(
|
|
|
|
|
Guid realmId,
|
|
|
|
|
int id,
|
|
|
|
|
CancellationToken ct = default)
|
|
|
|
|
{
|
|
|
|
|
return await db.Clients.FirstOrDefaultAsync(c => c.Id == id && c.RealmId == realmId, ct);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-16 19:15:04 +01:00
|
|
|
private ClientSecret CreateSecret(Realm realm)
|
2026-02-27 17:57:42 +00:00
|
|
|
{
|
|
|
|
|
Span<byte> secret = stackalloc byte[24];
|
|
|
|
|
RandomNumberGenerator.Fill(secret);
|
2026-03-16 19:15:04 +01:00
|
|
|
|
|
|
|
|
var dek = realm.DataEncryptionKeys.Single(k => k.Active);
|
2026-02-27 17:57:42 +00:00
|
|
|
|
|
|
|
|
return new ClientSecret()
|
|
|
|
|
{
|
|
|
|
|
CreatedAt = clock.UtcNow(),
|
2026-03-16 19:15:04 +01:00
|
|
|
Secret = cryptor.Encrypt(dek, secret),
|
2026-02-27 17:57:42 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
}
|