Still working on getting client credential flow complete, most of the request works but still working on generating the JWT.

This commit is contained in:
eelke 2026-03-16 19:15:04 +01:00
parent 1a8c63808a
commit 8782ef39c6
80 changed files with 1331 additions and 414 deletions

View file

@ -1,4 +1,5 @@
using Microsoft.Extensions.Logging.Abstractions;
using IdentityShroud.Core.EFCore;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Npgsql;
using Testcontainers.PostgreSql;

View file

@ -9,7 +9,6 @@
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4"/>
<PackageReference Include="jose-jwt" Version="5.2.0" />
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="10.0.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1"/>
<PackageReference Include="NSubstitute" Version="5.3.0" />

View file

@ -48,8 +48,7 @@ public class JwtSignatureGeneratorTests
}
]
}
""";
""";
JsonWebKeySet keySet = JsonSerializer.Deserialize<JsonWebKeySet>(keycloakKeySet)!;
using RSA publicKey = LoadFromJwk(keySet.Keys[0]);

View file

@ -1,5 +1,9 @@
using IdentityShroud.Api;
using IdentityShroud.Core.Contracts;
using IdentityShroud.Core.EFCore;
using IdentityShroud.Core.Model;
using IdentityShroud.Core.Security;
using IdentityShroud.Core.Security.Keys;
using IdentityShroud.Core.Services;
using IdentityShroud.Core.Tests.Fixtures;
using IdentityShroud.TestUtils.Substitutes;
@ -7,6 +11,29 @@ using Microsoft.EntityFrameworkCore;
namespace IdentityShroud.Core.Tests.Services;
public static class RealmDekBuilder
{
public static RealmDek DefaultActive() =>
new()
{
Id = DekId.NewId(),
Active = true,
Algorithm = KeyType.AES,
KeyData = new EncryptedDek(KekId.NewId(),
[
0
])
};
}
public static class ClientCreateRequestBuilder
{
public static ClientCreateRequest Default() => new(
"test-client",
"Test Client",
"A test client");
}
public class ClientServiceTests : IClassFixture<DbFixture>
{
private readonly DbFixture _dbFixture;
@ -34,15 +61,28 @@ public class ClientServiceTests : IClassFixture<DbFixture>
{
if (!db.Realms.Any(r => r.Id == _realmId))
{
db.Realms.Add(new() { Id = _realmId, Slug = "test-realm", Name = "Test Realm" });
db.Realms.Add(new()
{
Id = _realmId,
Slug = "test-realm",
Name = "Test Realm",
DataEncryptionKeys = [ RealmDekBuilder.DefaultActive(), ],
});
db.SaveChanges();
}
}
private ClientService CreateSut(Db db) => new(db,
_dataEncryptionService,
new ClientCreateRequestValidator(),
_clock);
[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task Create(bool allowClientCredentialsFlow)
public async Task Create(bool withSecret)
{
// Setup
DateTime now = DateTime.UtcNow;
@ -52,15 +92,13 @@ public class ClientServiceTests : IClassFixture<DbFixture>
await using (var db = _dbFixture.CreateDbContext())
{
// Act
ClientService sut = new(db, _dataEncryptionService, _clock);
ClientService sut = CreateSut(db);
var response = await sut.Create(
_realmId,
new ClientCreateRequest
ClientCreateRequestBuilder.Default() with
{
ClientId = "test-client",
Name = "Test Client",
Description = "A test client",
AllowClientCredentialsFlow = allowClientCredentialsFlow,
Confidential = withSecret,
GenerateSecret = withSecret,
},
TestContext.Current.CancellationToken);
@ -70,7 +108,7 @@ public class ClientServiceTests : IClassFixture<DbFixture>
Assert.Equal("test-client", val.ClientId);
Assert.Equal("Test Client", val.Name);
Assert.Equal("A test client", val.Description);
Assert.Equal(allowClientCredentialsFlow, val.AllowClientCredentialsFlow);
Assert.Equal(withSecret, val.Confidential);
Assert.Equal(now, val.CreatedAt);
}
@ -80,7 +118,7 @@ public class ClientServiceTests : IClassFixture<DbFixture>
.Include(e => e.Secrets)
.SingleAsync(e => e.Id == val.Id, TestContext.Current.CancellationToken);
if (allowClientCredentialsFlow)
if (withSecret)
Assert.Single(dbRecord.Secrets);
else
Assert.Empty(dbRecord.Secrets);
@ -108,7 +146,7 @@ public class ClientServiceTests : IClassFixture<DbFixture>
await using var actContext = _dbFixture.CreateDbContext();
// Act
ClientService sut = new(actContext, _dataEncryptionService, _clock);
ClientService sut = CreateSut(actContext);
Client? result = await sut.GetByClientId(_realmId, clientId, TestContext.Current.CancellationToken);
// Verify
@ -143,7 +181,7 @@ public class ClientServiceTests : IClassFixture<DbFixture>
await using var actContext = _dbFixture.CreateDbContext();
// Act
ClientService sut = new(actContext, _dataEncryptionService, _clock);
ClientService sut = CreateSut(actContext);
Client? result = await sut.FindById(_realmId, searchId, TestContext.Current.CancellationToken);
// Verify

View file

@ -2,6 +2,7 @@ using System.Security.Cryptography;
using IdentityShroud.Core.Contracts;
using IdentityShroud.Core.Model;
using IdentityShroud.Core.Security;
using IdentityShroud.Core.Security.Keys;
using IdentityShroud.Core.Services;
using IdentityShroud.TestUtils.Substitutes;
@ -9,23 +10,20 @@ namespace IdentityShroud.Core.Tests.Services;
public class DataEncryptionServiceTests
{
private readonly IRealmContext _realmContext = Substitute.For<IRealmContext>();
// private readonly IRealmContext _realmContext = Substitute.For<IRealmContext>();
private readonly IDekEncryptionService _dekCryptor = new NullDekEncryptionService();// Substitute.For<IDekEncryptionService>();
private readonly DekId _activeDekId = DekId.NewId();
private readonly DekId _secondDekId = DekId.NewId();
private DataEncryptionService CreateSut()
=> new(_realmContext, _dekCryptor);
=> new(_dekCryptor);
[Fact]
public void Encrypt_UsesActiveKey()
{
_realmContext.GetDeks(Arg.Any<CancellationToken>()).Returns([
CreateRealmDek(_secondDekId, false),
CreateRealmDek(_activeDekId, true),
]);
var cipher = CreateSut().Encrypt("Hello"u8);
var dek = CreateRealmDek(_activeDekId, true);
var cipher = CreateSut().Encrypt(dek, "Hello"u8);
Assert.Equal(_activeDekId, cipher.DekId);
}
@ -34,20 +32,18 @@ public class DataEncryptionServiceTests
public void Decrypt_UsesCorrectKey()
{
var first = CreateRealmDek(_activeDekId, true);
_realmContext.GetDeks(Arg.Any<CancellationToken>()).Returns([ first ]);
var sut = CreateSut();
var cipher = sut.Encrypt("Hello"u8);
var cipher = sut.Encrypt(first, "Hello"u8);
// Deactivate original key
first.Active = false;
// Make new active
var second = CreateRealmDek(_secondDekId, true);
// Return both
_realmContext.GetDeks(Arg.Any<CancellationToken>()).Returns([ first, second ]);
RealmDek[] list = [ first, second ];
var decoded = sut.Decrypt(cipher);
var decoded = sut.Decrypt(list, cipher);
Assert.Equal("Hello"u8, decoded);
}
@ -57,7 +53,7 @@ public class DataEncryptionServiceTests
{
Id = id,
Active = active,
Algorithm = "AES",
Algorithm = KeyType.AES,
KeyData = new(KekId.NewId(), RandomNumberGenerator.GetBytes(32)),
RealmId = default,
};

View file

@ -1,5 +1,4 @@
using IdentityShroud.Core.Security;
using IdentityShroud.Core.Services;
namespace IdentityShroud.Core.Tests.Services;

View file

@ -1,10 +1,13 @@
using FluentResults;
using IdentityShroud.Core.Contracts;
using IdentityShroud.Core.EFCore;
using IdentityShroud.Core.Model;
using IdentityShroud.Core.Security;
using IdentityShroud.Core.Security.Keys;
using IdentityShroud.Core.Services;
using IdentityShroud.Core.Tests.Fixtures;
using IdentityShroud.TestUtils.Substitutes;
using Microsoft.EntityFrameworkCore;
using Shouldly;
namespace IdentityShroud.Core.Tests.Services;
@ -12,6 +15,7 @@ public class RealmServiceTests : IClassFixture<DbFixture>
{
private readonly DbFixture _dbFixture;
private readonly IKeyService _keyService = Substitute.For<IKeyService>();
private readonly IDekEncryptionService _dekCryptor = new NullDekEncryptionService();
public RealmServiceTests(DbFixture dbFixture)
{
@ -25,6 +29,9 @@ public class RealmServiceTests : IClassFixture<DbFixture>
{
db.Database.ExecuteSqlRaw("TRUNCATE realm CASCADE;");
}
private RealmService CreateSut(Db db) => new(db, _keyService, _dekCryptor, new ClockService());
[Theory]
[InlineData(null)]
@ -36,20 +43,14 @@ public class RealmServiceTests : IClassFixture<DbFixture>
if (idString is not null)
realmId = new(idString);
RealmCreateResponse? val;
Realm? val;
await using (var db = _dbFixture.CreateDbContext())
{
_keyService.CreateKey(Arg.Any<KeyPolicy>())
.Returns(new RealmKey()
{
Id = Guid.NewGuid(),
KeyType = "TST",
Key = new(KekId.NewId(), [21]),
CreatedAt = DateTime.UtcNow
});
.Returns(new CreateKeyResponse(KeyType.AES, new KeyData([21])));
// Act
RealmService sut = new(db, _keyService);
var response = await sut.Create(
RealmService sut = CreateSut(db);
Result<Realm> response = await sut.Create(
new(realmId, "slug", "New realm"),
TestContext.Current.CancellationToken);
@ -60,8 +61,12 @@ public class RealmServiceTests : IClassFixture<DbFixture>
else
Assert.NotEqual(Guid.Empty, val.Id);
Assert.Equal("slug", val.Slug);
Assert.Equal("New realm", val.Name);
Assert.Multiple(
() => val.Slug.ShouldBe("slug"),
() => val.Name.ShouldBe("New realm"),
() => val.DataEncryptionKeys.ShouldContain(d => d.Active),
() => val.TokenSigningKeys.ShouldContain(d => !d.RevokedAt.HasValue)
);
_keyService.Received().CreateKey(Arg.Any<KeyPolicy>());
}
@ -69,9 +74,9 @@ public class RealmServiceTests : IClassFixture<DbFixture>
await using (var db = _dbFixture.CreateDbContext())
{
var dbRecord = await db.Realms
.Include(e => e.Keys)
.Include(e => e.TokenSigningKeys)
.SingleAsync(e => e.Id == val.Id, TestContext.Current.CancellationToken);
Assert.Equal("TST", dbRecord.Keys[0].KeyType);
Assert.Equal(KeyType.AES, dbRecord.TokenSigningKeys[0].KeyType);
}
}
@ -98,7 +103,7 @@ public class RealmServiceTests : IClassFixture<DbFixture>
await using var actContext = _dbFixture.CreateDbContext();
// Act
RealmService sut = new(actContext, _keyService);
RealmService sut = CreateSut(actContext);
var result = await sut.FindBySlug(slug, TestContext.Current.CancellationToken);
// Verify
@ -131,7 +136,7 @@ public class RealmServiceTests : IClassFixture<DbFixture>
await using var actContext = _dbFixture.CreateDbContext();
// Act
RealmService sut = new(actContext, _keyService);
RealmService sut = CreateSut(actContext);
Realm? result = await sut.FindById(id, TestContext.Current.CancellationToken);
// Verify

View file

@ -1,8 +1,7 @@
using System.Security.Cryptography;
using System.Text;
using System.Buffers.Text;
using System.Security.Cryptography;
using System.Text.Json;
using IdentityShroud.Core.DTO;
using Microsoft.AspNetCore.WebUtilities;
namespace IdentityShroud.Core.Tests;
@ -74,10 +73,10 @@ public static class JwtReader
return new JsonWebToken()
{
Header = JsonSerializer.Deserialize<JsonWebTokenHeader>(
Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(jwt, 0, firstDot)))!,
Base64Url.DecodeFromChars(jwt.AsSpan().Slice(0, firstDot)))!,
Payload = JsonSerializer.Deserialize<JsonWebTokenPayload>(
Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(jwt, firstDot + 1, secondDot - (firstDot + 1))))!,
Signature = WebEncoders.Base64UrlDecode(jwt, secondDot + 1, jwt.Length - (secondDot + 1))
Base64Url.DecodeFromChars(jwt.AsSpan().Slice(firstDot + 1, secondDot - (firstDot + 1))))!,
Signature = Base64Url.DecodeFromChars(jwt.AsSpan().Slice(secondDot + 1, jwt.Length - (secondDot + 1))),
};
}
}