IdentityShroud/IdentityShroud.Api.Tests/Apis/RealmApisTests.cs
eelke 644b005f2a Support rotation of master key.
The EncryptionService now loads a set of keys and uses the active one to encrypt and selects key based on keyid during decryption. Introduced EncryptedValue to hold keyId and encrypted data.

(There are no intermeddiate keys yet)
2026-02-24 06:32:58 +01:00

169 lines
No EOL
6.3 KiB
C#

using System.Net;
using System.Net.Http.Json;
using System.Security.Cryptography;
using System.Text.Json.Nodes;
using IdentityShroud.Core;
using IdentityShroud.Core.Contracts;
using IdentityShroud.Core.Model;
using IdentityShroud.Core.Tests.Fixtures;
using IdentityShroud.TestUtils.Asserts;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
namespace IdentityShroud.Api.Tests.Apis;
public class RealmApisTests : IClassFixture<ApplicationFactory>
{
private readonly ApplicationFactory _factory;
public RealmApisTests(ApplicationFactory factory)
{
_factory = factory;
using var scope = _factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<Db>();
if (!db.Database.EnsureCreated())
{
db.Database.ExecuteSqlRaw("TRUNCATE realm CASCADE;");
}
}
[Theory]
[InlineData(null, null, null, false, "Name")]
[InlineData(null, null, "Foo", true, "")]
[InlineData(null, null, "", false, "Name")]
[InlineData(null, "foo", "Foo", true, "")]
[InlineData(null, "f/oo", "Foo", false, "Slug")]
[InlineData(null, "", "Foo", false, "Slug")]
[InlineData("0814934a-efe2-4784-ba84-a184c0b9de9e", "foo", "Foo", true, "")]
[InlineData("00000000-0000-0000-0000-000000000000", "foo", "Foo", false, "Id")]
public async Task Create(string? id, string? slug, string? name, bool succeeds, string fieldName)
{
var client = _factory.CreateClient();
Guid? inputId = id is null ? (Guid?)null : new Guid(id);
// act
var response = await client.PostAsync("/api/v1/realms", JsonContent.Create(new
{
Id = inputId,
Slug = slug,
Name = name,
}),
TestContext.Current.CancellationToken);
#if DEBUG
string contents = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);
#endif
if (succeeds)
{
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
// await factory.RealmService.Received(1).Create(
// Arg.Is<RealmCreateRequest>(r => r.Id == inputId && r.Slug == slug && r.Name == name),
// Arg.Any<CancellationToken>());
}
else
{
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
var problemDetails =
await response.Content.ReadFromJsonAsync<ValidationProblemDetails>(
TestContext.Current.CancellationToken);
Assert.Contains(problemDetails!.Errors, e => e.Key == fieldName);
// await factory.RealmService.DidNotReceive().Create(
// Arg.Any<RealmCreateRequest>(),
// Arg.Any<CancellationToken>());
}
}
[Fact]
public async Task GetOpenIdConfiguration_Success()
{
// setup
await ScopedContextAsync(async db =>
{
db.Realms.Add(new Realm() { Slug = "foo", Name = "Foo" });
await db.SaveChangesAsync(TestContext.Current.CancellationToken);
});
// act
var client = _factory.CreateClient();
var response = await client.GetAsync("auth/realms/foo/.well-known/openid-configuration",
TestContext.Current.CancellationToken);
// verify
#if DEBUG
string contents = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);
#endif
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var result = await response.Content.ReadFromJsonAsync<JsonObject>(TestContext.Current.CancellationToken);
Assert.NotNull(result);
JsonObjectAssert.Equal("http://localhost/auth/realms/foo/openid-connect/auth", result, "authorization_endpoint");
JsonObjectAssert.Equal("http://localhost/auth/realms/foo", result, "issuer");
JsonObjectAssert.Equal("http://localhost/auth/realms/foo/openid-connect/token", result, "token_endpoint");
JsonObjectAssert.Equal("http://localhost/auth/realms/foo/openid-connect/jwks", result, "jwks_uri");
}
[Theory]
[InlineData("")]
[InlineData("bar")]
public async Task GetOpenIdConfiguration_NotFound(string slug)
{
// act
var client = _factory.CreateClient();
var response = await client.GetAsync($"/realms/{slug}/.well-known/openid-configuration",
TestContext.Current.CancellationToken);
// verify
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task GetJwks()
{
// setup
IEncryptionService encryptionService = _factory.Services.GetRequiredService<IEncryptionService>();
using var rsa = RSA.Create(2048);
RSAParameters parameters = rsa.ExportParameters(includePrivateParameters: false);
RealmKey realmKey = new()
{
Id = Guid.NewGuid(),
KeyType = "RSA",
Key = encryptionService.Encrypt(rsa.ExportPkcs8PrivateKey()),
CreatedAt = DateTime.UtcNow,
};
await ScopedContextAsync(async db =>
{
db.Realms.Add(new Realm() { Slug = "foo", Name = "Foo", Keys = [ realmKey ]});
await db.SaveChangesAsync(TestContext.Current.CancellationToken);
});
// act
var client = _factory.CreateClient();
var response = await client.GetAsync("/auth/realms/foo/openid-connect/jwks",
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
JsonObject? payload = await response.Content.ReadFromJsonAsync<JsonObject>(TestContext.Current.CancellationToken);
Assert.NotNull(payload);
JsonObjectAssert.Equal(realmKey.Id.ToString(), payload, "keys[0].kid");
JsonObjectAssert.Equal(WebEncoders.Base64UrlEncode(parameters.Modulus!), payload, "keys[0].n");
JsonObjectAssert.Equal(WebEncoders.Base64UrlEncode(parameters.Exponent!), payload, "keys[0].e");
}
private async Task ScopedContextAsync(
Func<Db, Task> action
)
{
using var scope = _factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<Db>();
await action(db);
}
}