using System.Security.Cryptography; using FluentValidation; using IdentityShroud.Core.Contracts; using IdentityShroud.Core.EFCore; using IdentityShroud.Core.Model; using Microsoft.EntityFrameworkCore; namespace IdentityShroud.Core.Services; public class ClientService( Db db, IDataEncryptionService cryptor, IValidator clientCreateValidator, IClock clock) : IClientService { public async Task> 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 is null ? null : new(request.SignatureAlgorithm), Confidential = request.Confidential, AllowClientCredentialsFlow = request.AllowClientCredentialsFlow, CreatedAt = clock.UtcNow(), }; if (request.GenerateSecret is true) { await db.Entry(realm).Collection(r => r.DataEncryptionKeys) .Query() .LoadAsync(ct); client.Secrets.Add(CreateSecret(realm)); } await db.AddAsync(client, ct); await db.SaveChangesAsync(ct); return client; } public async Task GetByClientId( Guid realmId, string clientId, CancellationToken ct = default) { return await db.Clients.FirstOrDefaultAsync(c => c.ClientId == clientId && c.RealmId == realmId, ct); } public async Task FindById( Guid realmId, int id, CancellationToken ct = default) { return await db.Clients.FirstOrDefaultAsync(c => c.Id == id && c.RealmId == realmId, ct); } private ClientSecret CreateSecret(Realm realm) { Span secret = stackalloc byte[24]; RandomNumberGenerator.Fill(secret); var dek = realm.DataEncryptionKeys.Single(k => k.Active); return new ClientSecret() { CreatedAt = clock.UtcNow(), Secret = cryptor.Encrypt(dek, secret), }; } }