diff --git a/.editorconfig b/.editorconfig
deleted file mode 100644
index 33a3ce8..0000000
--- a/.editorconfig
+++ /dev/null
@@ -1,2 +0,0 @@
-[*.cs]
-resharper_naming_rules.abbreviations = QL, DB
diff --git a/Directory.Packages.props b/Directory.Packages.props
deleted file mode 100644
index 653fdef..0000000
--- a/Directory.Packages.props
+++ /dev/null
@@ -1,36 +0,0 @@
-
-
- true
- true
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/IdentityShroud.Api.Tests/Apis/ClientApiTests.cs b/IdentityShroud.Api.Tests/Apis/ClientApiTests.cs
deleted file mode 100644
index cf1eb9f..0000000
--- a/IdentityShroud.Api.Tests/Apis/ClientApiTests.cs
+++ /dev/null
@@ -1,211 +0,0 @@
-using System.Net;
-using System.Net.Http.Json;
-using System.Text;
-using System.Text.Json;
-using FluentResults;
-using IdentityShroud.Core.Contracts;
-using IdentityShroud.Core.EFCore;
-using IdentityShroud.Core.Model;
-using IdentityShroud.Core.Tests;
-using IdentityShroud.Core.Tests.Fixtures;
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.Extensions.DependencyInjection;
-using Shouldly;
-
-namespace IdentityShroud.Api.Tests.Apis;
-
-public class ClientApiTests : IClassFixture
-{
- private readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
-
- private readonly ApplicationFactory _factory;
-
- public ClientApiTests(ApplicationFactory factory)
- {
- _factory = factory;
-
- using var scope = _factory.Services.CreateScope();
- var db = scope.ServiceProvider.GetRequiredService();
- if (!db.Database.EnsureCreated())
- {
- db.Database.ExecuteSqlRaw("TRUNCATE realm CASCADE;");
- }
- }
-
- [Theory]
- [InlineData(null, false, "ClientId")]
- [InlineData("", false, "ClientId")]
- [InlineData("my-client", true, "")]
- public async Task Create_Validation(string? clientId, bool succeeds, string fieldName)
- {
- // setup
- var realm = await CreateRealmAsync("test-realm", "Test Realm");
-
- var client = _factory.CreateClient();
-
- // act
- var response = await client.PostAsync(
- $"/api/v1/realms/{realm.Id}/clients",
- JsonContent.Create(new { ClientId = clientId }),
- TestContext.Current.CancellationToken);
-
-#if DEBUG
- string contents = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);
-#endif
-
- if (succeeds)
- {
- Assert.Equal(HttpStatusCode.Created, response.StatusCode);
- }
- else
- {
- Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
- var problemDetails =
- await response.Content.ReadFromJsonAsync(
- TestContext.Current.CancellationToken);
-
- Assert.Contains(problemDetails!.Errors, e => e.Key == fieldName);
- }
- }
-
- [Fact]
- public async Task Create_Success_ReturnsCreatedWithLocation()
- {
- // act
- var body = await DoCreateRequest("""
- {
- "clientId": "new-client",
- "name": "New Client"
- }
- """);
-
- // verify
- Assert.NotNull(body);
- Assert.Equal("new-client", body.ClientId);
- Assert.True(body.Id > 0);
- }
-
- [Fact]
- public async Task Create_Success_CreatesSecret()
- {
- // act
- var body = await DoCreateRequest("""
- {
- "clientId": "new-client",
- "name": "New Client",
- "confidential": true,
- "generateSecret": true
- }
- """);
-
- // verify
- body.ShouldNotBeNull();
- body.Secret.ShouldNotBeNullOrWhiteSpace();
- }
-
- private async Task DoCreateRequest(
- string request)
- {
- var realm = await CreateRealmAsync("create-realm", "Create Realm");
-
- var client = _factory.CreateClient();
- var response = await client.PostAsync(
- $"/api/v1/realms/{realm.Id}/clients",
- //JsonContent.Create(request),
- new StringContent(request, Encoding.UTF8, "application/json"),
- TestContext.Current.CancellationToken);
-
- string contents = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);
- Assert.True(HttpStatusCode.Created == response.StatusCode, contents);
-
- return JsonSerializer.Deserialize(contents, _jsonOptions);
- }
-
- [Fact]
- public async Task Create_UnknownRealm_ReturnsNotFound()
- {
- var client = _factory.CreateClient();
-
- var response = await client.PostAsync(
- $"/api/v1/realms/{Guid.NewGuid()}/clients",
- JsonContent.Create(new { ClientId = "some-client" }),
- TestContext.Current.CancellationToken);
-
- Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
- }
-
- [Fact]
- public async Task Get_Success()
- {
- // setup
- var realm = await CreateRealmAsync("get-realm", "Get Realm");
- Client dbClient = await CreateClientAsync(realm, "get-client", "Get Client");
-
- var httpClient = _factory.CreateClient();
-
- // act
- var response = await httpClient.GetAsync(
- $"/api/v1/realms/{realm.Id}/clients/{dbClient.Id}",
- TestContext.Current.CancellationToken);
-
-#if DEBUG
- string contents = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);
-#endif
-
- // verify
- Assert.Equal(HttpStatusCode.OK, response.StatusCode);
-
- var body = await response.Content.ReadFromJsonAsync(
- TestContext.Current.CancellationToken);
-
- Assert.NotNull(body);
- Assert.Equal(dbClient.Id, body.Id);
- Assert.Equal("get-client", body.ClientId);
- Assert.Equal("Get Client", body.Name);
- Assert.Equal(realm.Id, body.RealmId);
- }
-
- [Fact]
- public async Task Get_UnknownClient_ReturnsNotFound()
- {
- // setup
- var realm = await CreateRealmAsync("notfound-realm", "NotFound Realm");
-
- var httpClient = _factory.CreateClient();
-
- // act
- var response = await httpClient.GetAsync(
- $"/api/v1/realms/{realm.Id}/clients/99999",
- TestContext.Current.CancellationToken);
-
- // verify
- Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
- }
-
- private async Task CreateRealmAsync(string slug, string name)
- {
- using var scope = _factory.Services.CreateScope();
- var realmService = scope.ServiceProvider.GetRequiredService();
- Result result = await realmService.Create(
- new(null, slug, name),
- TestContext.Current.CancellationToken);
- return ResultAssert.Success(result);
- }
-
- private async Task CreateClientAsync(Realm realm, string clientId, string? name = null)
- {
- using var scope = _factory.Services.CreateScope();
- var db = scope.ServiceProvider.GetRequiredService();
- var client = new Client
- {
- RealmId = realm.Id,
- ClientId = clientId,
- Name = name,
- CreatedAt = DateTime.UtcNow,
- };
- db.Clients.Add(client);
- await db.SaveChangesAsync(TestContext.Current.CancellationToken);
- return client;
- }
-}
diff --git a/IdentityShroud.Api.Tests/Apis/OpenIdApiTests.cs b/IdentityShroud.Api.Tests/Apis/OpenIdApiTests.cs
deleted file mode 100644
index 93d241a..0000000
--- a/IdentityShroud.Api.Tests/Apis/OpenIdApiTests.cs
+++ /dev/null
@@ -1,123 +0,0 @@
-using System.Net;
-using System.Net.Http.Headers;
-using System.Net.Http.Json;
-using System.Text.Json.Serialization;
-using IdentityShroud.Api.Apis;
-using IdentityShroud.Core.EFCore;
-using IdentityShroud.Core.Tests.Fixtures;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.Extensions.DependencyInjection;
-using Shouldly;
-
-namespace IdentityShroud.Api.Tests.Apis;
-
-public class OpenIdApiTests : IClassFixture
-{
- private readonly ApplicationFactory _factory;
-
- public OpenIdApiTests(ApplicationFactory factory)
- {
- _factory = factory;
-
- using var scope = _factory.Services.CreateScope();
- var db = scope.ServiceProvider.GetRequiredService();
- if (!db.Database.EnsureCreated())
- {
- db.Database.ExecuteSqlRaw("TRUNCATE realm CASCADE;");
- }
- }
-
- [Theory]
- [InlineData(true)]
- [InlineData(false)]
- public async Task ClientCredentialsFlow(bool useAuthenticationHeader)
- {
- var client = _factory.CreateClient();
-
- var createRealmResponse = await client.PostAsync("/api/v1/realms", JsonContent.Create(new
- {
- Slug = "foo",
- Name = "Test'",
- }),
- TestContext.Current.CancellationToken);
-
- createRealmResponse.StatusCode.ShouldBe(HttpStatusCode.Created);
-
- var realm = await createRealmResponse.Content.ReadFromJsonAsync(
- cancellationToken: TestContext.Current.CancellationToken);
- realm.ShouldNotBeNull();
- realm.Id.ShouldNotBe(Guid.Empty);
-
- var createClientResponse = await client.PostAsync(
- $"/api/v1/realms/{realm.Id}/clients",
- JsonContent.Create(new
- {
- ClientId = "myclient",
- Name = "New Client",
- Confidential = true,
- AllowClientCredentialsFlow = true,
- GenerateSecret = true,
- }),
- TestContext.Current.CancellationToken);
-
- createClientResponse.StatusCode.ShouldBe(HttpStatusCode.Created);
-
- // Act
- const string clientId = "myclient";
-
- var data = new[]
- {
- new KeyValuePair("client_id", clientId),
- new KeyValuePair("client_secret", "secret"),
- new KeyValuePair("response_type", "token"),
- new KeyValuePair("grant_type", "client_credentials"),
- };
-
- if (useAuthenticationHeader)
- {
- // client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("basic",
- // Convert.ToBase64String($"{clientId}:{clientSecret}"))
- }
-
- var content = new FormUrlEncodedContent(data);
- var response = await client.PostAsync(
- "/auth/realms/foo/openid-connect/token",
- content,
- TestContext.Current.CancellationToken);
-
- // Verify
- // var responseJson = await response.Content.ReadAsStringAsync(
- // TestContext.Current.CancellationToken);
- // Console.WriteLine($"Response: {responseJson}");
-
- response.StatusCode.ShouldBe(HttpStatusCode.OK);
-
- // Cache-Control: no-store
- response.Headers.CacheControl.ShouldNotBeNull()
- .NoStore.ShouldBe(true);
- // Pragma: no-cache
- response.Headers.Pragma.ShouldNotBeNull()
- .ShouldContain(new NameValueHeaderValue("no-cache"));
-
- var payload = await response.Content.ReadFromJsonAsync();
- payload.ShouldNotBeNull();
- Assert.Multiple(
- () => payload.AccessToken.ShouldNotBeNull(),
- () => payload.TokenType.ShouldBe("bearer"),
- () => payload.ExpiresIn.ShouldBe(3600));
-
- // - refresh_token OPTIONAL
- // - scope OPTIONAL when identical to request otherwise REQUIRED
- }
-
- internal class TokenResponse
- {
- [JsonPropertyName("access_token")]
- public string? AccessToken { get; set; }
- [JsonPropertyName("token_type")]
- public string? TokenType { get; set; }
- [JsonPropertyName("expires_in")]
- public int? ExpiresIn { get; set; }
- }
-
-}
\ No newline at end of file
diff --git a/IdentityShroud.Api.Tests/Apis/RealmApisTests.cs b/IdentityShroud.Api.Tests/Apis/RealmApisTests.cs
index 7d3d49e..7e6192e 100644
--- a/IdentityShroud.Api.Tests/Apis/RealmApisTests.cs
+++ b/IdentityShroud.Api.Tests/Apis/RealmApisTests.cs
@@ -1,34 +1,16 @@
-using System.Buffers.Text;
using System.Net;
using System.Net.Http.Json;
-using System.Security.Cryptography;
-using System.Text.Json.Nodes;
-using IdentityShroud.Core.EFCore;
-using IdentityShroud.Core.Model;
+using FluentResults;
+using IdentityShroud.Core.Messages.Realm;
+using IdentityShroud.Core.Services;
using IdentityShroud.Core.Tests.Fixtures;
-using IdentityShroud.TestUtils.Asserts;
using Microsoft.AspNetCore.Mvc;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.Extensions.DependencyInjection;
+using NSubstitute.ClearExtensions;
namespace IdentityShroud.Api.Tests.Apis;
-public class RealmApisTests : IClassFixture
+public class RealmApisTests(ApplicationFactory factory) : IClassFixture
{
- private readonly ApplicationFactory _factory;
-
- public RealmApisTests(ApplicationFactory factory)
- {
- _factory = factory;
-
- using var scope = _factory.Services.CreateScope();
- var db = scope.ServiceProvider.GetRequiredService();
- if (!db.Database.EnsureCreated())
- {
- db.Database.ExecuteSqlRaw("TRUNCATE realm CASCADE;");
- }
- }
-
[Theory]
[InlineData(null, null, null, false, "Name")]
[InlineData(null, null, "Foo", true, "")]
@@ -40,142 +22,40 @@ public class RealmApisTests : IClassFixture
[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();
+ var client = factory.CreateClient();
+
+ factory.RealmService.ClearSubstitute();
+ factory.RealmService.Create(Arg.Any(), Arg.Any())
+ .Returns(Result.Ok(new RealmCreateResponse(Guid.NewGuid(), "foo", "Foo")));
Guid? inputId = id is null ? (Guid?)null : new Guid(id);
-
- // act
- var response = await client.PostAsync("/api/v1/realms", JsonContent.Create(new
+ var response = await client.PostAsync("/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
-
+ 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(r => r.Id == inputId && r.Slug == slug && r.Name == name),
- // Arg.Any());
+ await factory.RealmService.Received(1).Create(
+ Arg.Is(r => r.Id == inputId && r.Slug == slug && r.Name == name),
+ Arg.Any());
}
else
{
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
- var problemDetails =
- await response.Content.ReadFromJsonAsync(
- TestContext.Current.CancellationToken);
+ var problemDetails = await response.Content.ReadFromJsonAsync(TestContext.Current.CancellationToken);
Assert.Contains(problemDetails!.Errors, e => e.Key == fieldName);
- // await factory.RealmService.DidNotReceive().Create(
- // Arg.Any(),
- // Arg.Any());
+ await factory.RealmService.DidNotReceive().Create(
+ Arg.Any(),
+ Arg.Any());
}
}
-
- [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(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()
- {
- var client = _factory.CreateClient();
- var createResponse = await client.PostAsync("/api/v1/realms", JsonContent.Create(new
- {
- Slug = "foo",
- Name = "Test'",
- }),
- TestContext.Current.CancellationToken);
- Assert.Equal(HttpStatusCode.Created, createResponse.StatusCode);
-
- // act
- 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(TestContext.Current.CancellationToken);
-
- Assert.NotNull(payload);
- string? kid = JsonObjectAssert.NavigateToPath(payload, "keys[0].kid")?.AsValue().ToString();
- Assert.NotNull(kid);
- Assert.True(kid.Length >= 16);
-
- //if (JsonObjectAssert.NavigateToPath(payload, "keys[0].kty")?.AsValue().ToString() == "RSA")
-
- JsonObjectAssert.Equal("RSA", payload, "keys[0].kty");
- string? n = payload["keys"]?[0]?["n"]?.AsValue().ToString();
- string? e = payload["keys"]?[0]?["e"]?.AsValue().ToString();
- AssertRsaParams(n, e);
- }
-
- private async Task ScopedContextAsync(
- Func action
- )
- {
- using var scope = _factory.Services.CreateScope();
- var db = scope.ServiceProvider.GetRequiredService();
- await action(db);
- }
-
- private static void AssertRsaParams(string? n, string? e)
- {
- Assert.NotNull(n);
- Assert.NotNull(e);
-
- var rsa = RSA.Create();
- rsa.ImportParameters(new RSAParameters
- {
- Modulus = Base64Url.DecodeFromChars(n),
- Exponent = Base64Url.DecodeFromChars(e)
- });
-
- // If n and e are complete nonsense, this will throw
- var encrypted = rsa.Encrypt(new byte[] { 1, 2, 3 }, RSAEncryptionPadding.OaepSHA256);
- Assert.NotNull(encrypted);
- Assert.NotEmpty(encrypted);
- }
}
\ No newline at end of file
diff --git a/IdentityShroud.Api.Tests/Fixtures/ApplicationFactory.cs b/IdentityShroud.Api.Tests/Fixtures/ApplicationFactory.cs
index 0c5337d..6135df6 100644
--- a/IdentityShroud.Api.Tests/Fixtures/ApplicationFactory.cs
+++ b/IdentityShroud.Api.Tests/Fixtures/ApplicationFactory.cs
@@ -1,57 +1,24 @@
-using IdentityShroud.Api;
+using IdentityShroud.Core.Services;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
-using Microsoft.Extensions.Configuration;
-using Testcontainers.PostgreSql;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.VisualStudio.TestPlatform.TestHost;
namespace IdentityShroud.Core.Tests.Fixtures;
-public class ApplicationFactory : WebApplicationFactory, IAsyncLifetime
+public class ApplicationFactory : WebApplicationFactory
{
- private readonly PostgreSqlContainer _postgresqlServer;
+ public IRealmService RealmService { get; } = Substitute.For();
-// public IRealmService RealmService { get; } = Substitute.For();
-
- public ApplicationFactory()
- {
- _postgresqlServer = new PostgreSqlBuilder("postgres:18.1")
- .WithName($"is-applicationFactory-{Guid.NewGuid():N}")
- .Build();
- }
-
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
base.ConfigureWebHost(builder);
- builder.ConfigureAppConfiguration((context, configBuilder) =>
+ builder.ConfigureServices(services =>
{
- configBuilder.AddInMemoryCollection(
- new Dictionary
- {
- ["Db:ConnectionString"] = _postgresqlServer.GetConnectionString(),
- ["secrets:master:0:Id"] = "94970f27-3d88-4223-9940-7dd57548f5b5",
- ["secrets:master:0:Active"] = "true",
- ["secrets:master:0:Algorithm"] = "AES",
- ["secrets:master:0:Key"] = "GVd07qW0frRX9quPX/X62L88BeRR7+IzgRJHtG7ZzHw=",
- });
+ services.AddScoped(c => RealmService);
});
- // builder.ConfigureServices(services =>
- // {
- // services.AddScoped(c => RealmService);
- // });
-
builder.UseEnvironment("Development");
}
-
- public async ValueTask InitializeAsync()
- {
- await _postgresqlServer.StartAsync();
- }
-
- public override async ValueTask DisposeAsync()
- {
- await _postgresqlServer.StopAsync();
- await base.DisposeAsync();
- }
}
\ No newline at end of file
diff --git a/IdentityShroud.Api.Tests/HeaderHelpersTests.cs b/IdentityShroud.Api.Tests/HeaderHelpersTests.cs
deleted file mode 100644
index c08303a..0000000
--- a/IdentityShroud.Api.Tests/HeaderHelpersTests.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-using IdentityShroud.Api.Helpers;
-
-namespace IdentityShroud.Api.Tests;
-
-public class HeaderHelpersTests
-{
- [Theory]
- [InlineData("Basic dXNlcjpzZWNyZXQ=", true, "user", "secret")]
- [InlineData("baSIC dXNlcjpzZWNyZXQ=", true, "user", "secret")]
- [InlineData("Basic dXNlcnNlY3JldA==", false, null, null)] // no colon to seperate user and password
- [InlineData("Bearer dXNlcjpzZWNyZXQ=", false, null, null)]
- public void TryDecodeBasicAuth(string input, bool expectedResult, string? expectedUser, string? expectedPassword)
- {
- var result = HeaderHelpers.TryDecodeBasicAuth(input, out string? user, out string? password);
-
- Assert.Equal(expectedResult, result);
- Assert.Equal(expectedUser, user);
- Assert.Equal(expectedPassword, password);
- }
-}
\ No newline at end of file
diff --git a/IdentityShroud.Api.Tests/IdentityShroud.Api.Tests.csproj b/IdentityShroud.Api.Tests/IdentityShroud.Api.Tests.csproj
index 67cca0e..6351c40 100644
--- a/IdentityShroud.Api.Tests/IdentityShroud.Api.Tests.csproj
+++ b/IdentityShroud.Api.Tests/IdentityShroud.Api.Tests.csproj
@@ -1,4 +1,4 @@
-
+
net10.0
@@ -8,25 +8,24 @@
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
+
+
-
+
-
diff --git a/IdentityShroud.Api/Apis/ClientApi.cs b/IdentityShroud.Api/Apis/ClientApi.cs
deleted file mode 100644
index 05b4aa7..0000000
--- a/IdentityShroud.Api/Apis/ClientApi.cs
+++ /dev/null
@@ -1,97 +0,0 @@
-using FluentResults;
-using IdentityShroud.Api.Mappers;
-using IdentityShroud.Core.Contracts;
-using IdentityShroud.Core.Model;
-using Microsoft.AspNetCore.Http.HttpResults;
-using Microsoft.AspNetCore.Mvc;
-
-namespace IdentityShroud.Api.Apis;
-
-///
-/// The part of the api below realms/{slug}/clients
-///
-public static class ClientApi
-{
- public const string ClientGetRouteName = "ClientGet";
-
- public static void MapEndpoints(this IEndpointRouteBuilder erp)
- {
- RouteGroupBuilder clientsGroup = erp.MapGroup("clients");
-
- clientsGroup.MapPost("", ClientCreate)
- .Produces(StatusCodes.Status201Created)
- .Validate()
- .WithName("ClientCreate");
-
- var clientIdGroup = clientsGroup.MapGroup("{clientId}")
- .AddEndpointFilter();
-
- clientIdGroup.MapGet("", ClientGet)
- .WithName(ClientGetRouteName);
- }
-
- private static Ok ClientGet(
- Guid realmId,
- int clientId,
- HttpContext context)
- {
- Client client = (Client)context.Items["ClientEntity"]!;
- return TypedResults.Ok(new ClientMapper().ToDto(client));
- }
-
- private static async Task, InternalServerError>>
- ClientCreate(
- Guid realmId,
- ClientCreateRequest request,
- [FromServices] IClientService service,
- [FromServices] IDataEncryptionService cryptor,
- HttpContext context,
- CancellationToken cancellationToken)
- {
- Realm realm = context.GetValidatedRealm();
- Result result = await service.Create(realm.Id, request, cancellationToken);
-
- if (result.IsFailed)
- {
- throw new NotImplementedException();
- }
-
- Client client = result.Value;
- ClientRepresentation clientRepresentation = new ClientMapper().ToDto(client);
- var secret = SelectBestSecret(client.Secrets);
- if (secret is {} s)
- clientRepresentation.Secret = cryptor.DecryptUtf8ToString(realm.DataEncryptionKeys, s.Secret);
- return TypedResults.CreatedAtRoute(
- clientRepresentation,
- ClientGetRouteName,
- new RouteValueDictionary()
- {
- ["realmId"] = realm.Id,
- ["clientId"] = client.Id,
- });
- }
-
- private static ClientSecret? SelectBestSecret(List clientSecrets)
- {
- ClientSecret? result = null;
-
- foreach (var cs in clientSecrets)
- {
- if (cs.RevokedAt is null && (!cs.Expires.HasValue || cs.Expires.Value > DateTime.UtcNow))
- {
- if (result is null)
- {
- result = cs;
- }
- else
- {
- int d = (cs.Expires ?? DateTime.MaxValue).CompareTo(result.Expires ?? DateTime.MaxValue);
- if (d > 0 || (d == 0 && cs.CreatedAt > result.CreatedAt))
- result = cs;
- }
- }
- }
-
- return result;
- }
-}
\ No newline at end of file
diff --git a/IdentityShroud.Api/Apis/Dto/ClientRepresentation.cs b/IdentityShroud.Api/Apis/Dto/ClientRepresentation.cs
deleted file mode 100644
index d5e2853..0000000
--- a/IdentityShroud.Api/Apis/Dto/ClientRepresentation.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-namespace IdentityShroud.Api;
-
-public record ClientRepresentation
-{
- public int Id { get; set; }
- public Guid RealmId { get; set; }
- public required string ClientId { get; set; }
- public string? Name { get; set; }
- public string? Description { get; set; }
-
- public string? SignatureAlgorithm { get; set; }
-
- public bool Confidential { get; set; }
- public bool AllowClientCredentialsFlow { get; set; } = false;
-
- public required DateTime CreatedAt { get; set; }
-
- public string? Secret { get; set; }
-}
\ No newline at end of file
diff --git a/IdentityShroud.Api/Apis/Dto/ErrorDto.cs b/IdentityShroud.Api/Apis/Dto/ErrorDto.cs
deleted file mode 100644
index 655d4c4..0000000
--- a/IdentityShroud.Api/Apis/Dto/ErrorDto.cs
+++ /dev/null
@@ -1,3 +0,0 @@
-namespace IdentityShroud.Api.Apis;
-
-public record ErrorDto(string Error);
diff --git a/IdentityShroud.Api/Apis/Dto/RealmRepresentation.cs b/IdentityShroud.Api/Apis/Dto/RealmRepresentation.cs
deleted file mode 100644
index 29f6ca5..0000000
--- a/IdentityShroud.Api/Apis/Dto/RealmRepresentation.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-namespace IdentityShroud.Api.Apis;
-
-public record RealmRepresentation(
- Guid Id,
- string Slug,
- string Name);
\ No newline at end of file
diff --git a/IdentityShroud.Api/Apis/Dto/TokenRequestBody.cs b/IdentityShroud.Api/Apis/Dto/TokenRequestBody.cs
deleted file mode 100644
index 88672d6..0000000
--- a/IdentityShroud.Api/Apis/Dto/TokenRequestBody.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using System.Text.Json.Serialization;
-
-namespace IdentityShroud.Core.DTO.OpenId;
-
-public class TokenRequestBody
-{
- [JsonPropertyName("grant_type")]
- public GrantTypes GrantType { get; init; }
-
- ///
- /// In most cases required but not when basic auth header is used
- ///
- [JsonPropertyName("client_id")]
- public string? ClientId { get; init; } = "";
-
- [JsonPropertyName("client_secret")]
- public string? ClientSecret { get; init; }
-
- [JsonPropertyName("scope")]
- public string? Scope { get; init; }
-}
\ No newline at end of file
diff --git a/IdentityShroud.Api/Apis/EndpointRouteBuilderExtensions.cs b/IdentityShroud.Api/Apis/EndpointRouteBuilderExtensions.cs
deleted file mode 100644
index 5e2590f..0000000
--- a/IdentityShroud.Api/Apis/EndpointRouteBuilderExtensions.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-namespace IdentityShroud.Api;
-
-public static class EndpointRouteBuilderExtensions
-{
- public static IEndpointConventionBuilder Validate(this IEndpointConventionBuilder builder)
- where TDto : class
- => builder.AddEndpointFilter>();
-
- public static void MapApis(this IEndpointRouteBuilder erp)
- {
- RealmApi.MapRealmEndpoints(erp);
-
- OpenIdEndpoints.MapEndpoints(erp);
- }
-
-}
\ No newline at end of file
diff --git a/IdentityShroud.Api/Apis/Filters/ClientIdValidationFilter.cs b/IdentityShroud.Api/Apis/Filters/ClientIdValidationFilter.cs
deleted file mode 100644
index 771be81..0000000
--- a/IdentityShroud.Api/Apis/Filters/ClientIdValidationFilter.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using IdentityShroud.Core.Contracts;
-using IdentityShroud.Core.Model;
-
-namespace IdentityShroud.Api;
-
-public class ClientIdValidationFilter(IClientService clientService) : IEndpointFilter
-{
- public async ValueTask
diff --git a/IdentityShroud.Api/IdentityShroud.Api.csproj.DotSettings b/IdentityShroud.Api/IdentityShroud.Api.csproj.DotSettings
deleted file mode 100644
index c9c4f6a..0000000
--- a/IdentityShroud.Api/IdentityShroud.Api.csproj.DotSettings
+++ /dev/null
@@ -1,5 +0,0 @@
-
- True
- True
- True
- True
\ No newline at end of file
diff --git a/IdentityShroud.Api/Program.cs b/IdentityShroud.Api/Program.cs
index 2ff5fe6..510c626 100644
--- a/IdentityShroud.Api/Program.cs
+++ b/IdentityShroud.Api/Program.cs
@@ -1,74 +1,60 @@
using FluentValidation;
-using IdentityShroud.Api.Mappers;
+using IdentityShroud.Api;
+using IdentityShroud.Api.Validation;
using IdentityShroud.Core;
-using IdentityShroud.Core.EFCore;
-using IdentityShroud.GraphQL;
+using IdentityShroud.Core.Contracts;
+using IdentityShroud.Core.Security;
using Serilog;
using Serilog.Formatting.Json;
+
// Initial logging until we can set it up from Configuration
+Log.Logger = new LoggerConfiguration()
+ .Enrich.FromLogContext()
+ .WriteTo.Console(new JsonFormatter())
+ .CreateLogger();
-namespace IdentityShroud.Api;
+var applicationBuilder = WebApplication.CreateSlimBuilder(args);
+ConfigureBuilder(applicationBuilder);
+var application = applicationBuilder.Build();
+ConfigureApplication(application);
+application.Run();
-public class Program
+void ConfigureBuilder(WebApplicationBuilder builder)
{
- public static void Main(string[] args)
+ var services = builder.Services;
+ var configuration = builder.Configuration;
+
+ //services.AddControllers();
+ services.ConfigureHttpJsonOptions(options =>
{
- Log.Logger = new LoggerConfiguration()
- .Enrich.FromLogContext()
- .WriteTo.Console(new JsonFormatter())
- .CreateLogger();
+ options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default);
+ });
- var applicationBuilder = WebApplication.CreateSlimBuilder(args);
- ConfigureBuilder(applicationBuilder);
- var application = applicationBuilder.Build();
- ConfigureApplication(application);
- application.Run();
- }
+ // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
+ services.AddOpenApi();
+ services.AddScoped();
+ services.AddOptions().Bind(configuration.GetSection("db"));
+ services.AddSingleton();
+
+ services.AddValidatorsFromAssemblyContaining();
+
+ builder.Host.UseSerilog((context, services, configuration) => configuration
+ .Enrich.FromLogContext()
+ //.Enrich.With()
+ .ReadFrom.Configuration(context.Configuration));
+}
- private static void ConfigureBuilder(WebApplicationBuilder builder)
+void ConfigureApplication(WebApplication app)
+{
+ if (app.Environment.IsDevelopment())
{
- var services = builder.Services;
- var configuration = builder.Configuration;
-
- services.AddOptions().Bind(configuration.GetSection("db"));
-
- // services.ConfigureHttpJsonOptions(options =>
- // {
- // options.SerializerOptions.TypeInfoResolverChain.Insert(0, IdentityShroud.Api.AppJsonSerializerContext.Default);
- // });
-
- services.AddScoped();
-
- services.AddValidatorsFromAssemblyContaining();
-
- services.AddHttpContextAccessor();
- services.AddOpenApi();
- services.AddExceptionHandler();
- services.AddProblemDetails();
-
- services
- .AddCore()
- .AddIdentityShroudGraphQL();
-
- builder.Host.UseSerilog((context, services, configuration) => configuration
- .Enrich.FromLogContext()
- //.Enrich.With()
- .ReadFrom.Configuration(context.Configuration));
+ app.MapOpenApi();
}
+ app.UseSerilogRequestLogging();
+ app.MapRealmEndpoints();
+ // app.UseRouting();
+ // app.MapControllers();
+}
- private static void ConfigureApplication(WebApplication app)
- {
- app.UseExceptionHandler();
- if (app.Environment.IsDevelopment())
- {
- app.MapOpenApi();
- }
- app.UseSerilogRequestLogging();
- app.MapApis();
- app.MapIdentityShroudGraphQL();
-
- // app.UseRouting();
- // app.MapControllers();
- }
-}
\ No newline at end of file
+public partial class Program { }
diff --git a/IdentityShroud.Api/Properties/launchSettings.json b/IdentityShroud.Api/Properties/launchSettings.json
index 8556497..9472c5a 100644
--- a/IdentityShroud.Api/Properties/launchSettings.json
+++ b/IdentityShroud.Api/Properties/launchSettings.json
@@ -5,7 +5,7 @@
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
- "launchUrl": "graphql",
+ "launchUrl": "todos",
"applicationUrl": "http://localhost:5249",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
diff --git a/IdentityShroud.Api/Validation/EndpointRouteBuilderExtensions.cs b/IdentityShroud.Api/Validation/EndpointRouteBuilderExtensions.cs
new file mode 100644
index 0000000..e67f787
--- /dev/null
+++ b/IdentityShroud.Api/Validation/EndpointRouteBuilderExtensions.cs
@@ -0,0 +1,7 @@
+namespace IdentityShroud.Api.Validation;
+
+public static class EndpointRouteBuilderExtensions
+{
+ public static RouteHandlerBuilder Validate(this RouteHandlerBuilder builder) where TDto : class
+ => builder.AddEndpointFilter>();
+}
\ No newline at end of file
diff --git a/IdentityShroud.Api/Apis/Validation/RealmCreateRequestValidator.cs b/IdentityShroud.Api/Validation/RealmCreateRequestValidator.cs
similarity index 92%
rename from IdentityShroud.Api/Apis/Validation/RealmCreateRequestValidator.cs
rename to IdentityShroud.Api/Validation/RealmCreateRequestValidator.cs
index 3e3a20a..8daa0a9 100644
--- a/IdentityShroud.Api/Apis/Validation/RealmCreateRequestValidator.cs
+++ b/IdentityShroud.Api/Validation/RealmCreateRequestValidator.cs
@@ -1,7 +1,7 @@
using FluentValidation;
using IdentityShroud.Core.Messages.Realm;
-namespace IdentityShroud.Api;
+namespace IdentityShroud.Api.Validation;
public class RealmCreateRequestValidator : AbstractValidator
{
diff --git a/IdentityShroud.Api/Apis/Filters/ValidateFilter.cs b/IdentityShroud.Api/Validation/ValidateFilter.cs
similarity index 96%
rename from IdentityShroud.Api/Apis/Filters/ValidateFilter.cs
rename to IdentityShroud.Api/Validation/ValidateFilter.cs
index d621441..fbebd9d 100644
--- a/IdentityShroud.Api/Apis/Filters/ValidateFilter.cs
+++ b/IdentityShroud.Api/Validation/ValidateFilter.cs
@@ -1,6 +1,6 @@
using FluentValidation;
-namespace IdentityShroud.Api;
+namespace IdentityShroud.Api.Validation;
public class ValidateFilter : IEndpointFilter where T : class
{
diff --git a/IdentityShroud.TestUtils/Asserts/ResultAssert.cs b/IdentityShroud.Core.Tests/Asserts/ResultAssert.cs
similarity index 100%
rename from IdentityShroud.TestUtils/Asserts/ResultAssert.cs
rename to IdentityShroud.Core.Tests/Asserts/ResultAssert.cs
diff --git a/IdentityShroud.Core.Tests/Fixtures/DbFixture.cs b/IdentityShroud.Core.Tests/Fixtures/DbFixture.cs
index 1df6559..573fb8b 100644
--- a/IdentityShroud.Core.Tests/Fixtures/DbFixture.cs
+++ b/IdentityShroud.Core.Tests/Fixtures/DbFixture.cs
@@ -1,4 +1,4 @@
-using IdentityShroud.Core.EFCore;
+using DotNet.Testcontainers.Containers;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Npgsql;
@@ -8,13 +8,23 @@ namespace IdentityShroud.Core.Tests.Fixtures;
public class DbFixture : IAsyncLifetime
{
- private readonly PostgreSqlContainer _postgresqlServer;
+ private readonly IContainer _postgresqlServer;
- public Db CreateDbContext(string dbName = "testdb")
+ private string ConnectionString =>
+ $"Host={_postgresqlServer.Hostname};" +
+ $"Port={DbPort};" +
+ $"Username={Username};Password={Password}";
+
+ private string Username => "postgres";
+ private string Password => "password";
+ private string DbHostname => _postgresqlServer.Hostname;
+ private int DbPort => _postgresqlServer.GetMappedPublicPort(PostgreSqlBuilder.PostgreSqlPort);
+
+ public Db CreateDbContext(string dbName)
{
var db = new Db(Options.Create(new()
{
- ConnectionString = _postgresqlServer.GetConnectionString(),
+ ConnectionString = ConnectionString + ";Database=" + dbName,
LogSensitiveData = false,
}), new NullLoggerFactory());
return db;
@@ -23,7 +33,8 @@ public class DbFixture : IAsyncLifetime
public DbFixture()
{
_postgresqlServer = new PostgreSqlBuilder("postgres:18.1")
- .WithName("is-dbfixture-" + Guid.NewGuid().ToString("D"))
+ .WithName("KMS-Test-Infra-" + Guid.NewGuid().ToString("D"))
+ .WithPassword(Password)
.Build();
}
@@ -39,7 +50,7 @@ public class DbFixture : IAsyncLifetime
public NpgsqlConnection GetConnection(string dbname)
{
- string connString = _postgresqlServer.GetConnectionString()
+ string connString = ConnectionString
+ $";Database={dbname}";
var connection = new NpgsqlConnection(connString);
connection.Open();
diff --git a/IdentityShroud.Core.Tests/Helpers/Base64UrlConverterTests.cs b/IdentityShroud.Core.Tests/Helpers/Base64UrlConverterTests.cs
deleted file mode 100644
index 923a865..0000000
--- a/IdentityShroud.Core.Tests/Helpers/Base64UrlConverterTests.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-using System.Text;
-using System.Text.Json;
-using System.Text.Json.Serialization;
-using IdentityShroud.Core.Helpers;
-
-namespace IdentityShroud.Core.Tests.Helpers;
-
-public class Base64UrlConverterTests
-{
- internal class Data
- {
- [JsonConverter(typeof(Base64UrlConverter))]
- public byte[]? X { get; set; }
- }
-
- [Fact]
- public void Serialize()
- {
- Data d = new() { X = ">>>???"u8.ToArray() };
- string s = JsonSerializer.Serialize(d);
-
- Assert.Contains("\"Pj4-Pz8_\"", s);
- }
-
- [Fact]
- public void Deerialize()
- {
- var jsonstring = """
- { "X": "Pj4-Pz8_" }
- """;
- var d = JsonSerializer.Deserialize(jsonstring);
-
- Assert.Equal(">>>???", Encoding.UTF8.GetString(d.X));
- }
-
-}
\ No newline at end of file
diff --git a/IdentityShroud.Core.Tests/Helpers/SlugHelperTests.cs b/IdentityShroud.Core.Tests/Helpers/SlugHelperTests.cs
deleted file mode 100644
index de94511..0000000
--- a/IdentityShroud.Core.Tests/Helpers/SlugHelperTests.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using IdentityShroud.Core.Helpers;
-
-namespace IdentityShroud.Core.Tests.Helpers;
-
-public class SlugHelperTests
-{
- [Theory]
- [InlineData("", 40, "")]
- [InlineData("test", 40, "test")]
- [InlineData("Test", 40, "test")]
- [InlineData("tést", 40, "test")]
- [InlineData("foo_bar", 40, "foo-bar")]
- [InlineData("foo bar", 40, "foo-bar")]
- [InlineData("-foo", 40, "foo")]
- [InlineData("foo-", 40, "foo")]
- [InlineData("_foo", 40, "foo")]
- [InlineData("foo_", 40, "foo")]
- [InlineData("slug_would_be_too_long", 16, "slug-woul-frYeRw")] // not at word boundary
- [InlineData("slug_would_be_too_long", 18, "slug-would-frYeRw")] // at word boundary
- public void Test(string input, int max_length, string expected)
- {
- string result = SlugHelper.GenerateSlug(input, max_length);
-
- Assert.Equal(expected, result);
- }
-}
\ No newline at end of file
diff --git a/IdentityShroud.Core.Tests/IdentityShroud.Core.Tests.csproj b/IdentityShroud.Core.Tests/IdentityShroud.Core.Tests.csproj
index 918119c..3cb7db3 100644
--- a/IdentityShroud.Core.Tests/IdentityShroud.Core.Tests.csproj
+++ b/IdentityShroud.Core.Tests/IdentityShroud.Core.Tests.csproj
@@ -1,4 +1,4 @@
-
+
net10.0
@@ -8,25 +8,24 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
+
+
-
-
\ No newline at end of file
diff --git a/IdentityShroud.Core.Tests/JwtSignatureGeneratorTests.cs b/IdentityShroud.Core.Tests/JwtSignatureGeneratorTests.cs
deleted file mode 100644
index 4563ea4..0000000
--- a/IdentityShroud.Core.Tests/JwtSignatureGeneratorTests.cs
+++ /dev/null
@@ -1,82 +0,0 @@
-using System.Security.Cryptography;
-using System.Text;
-using System.Text.Json;
-using IdentityShroud.Core.Messages;
-using Microsoft.AspNetCore.WebUtilities;
-
-namespace IdentityShroud.Core.Tests;
-
-public class JwtSignatureGeneratorTests
-{
-
- [Fact]
- public void VerifySignatureValid()
- {
- using var rsa = RSA.Create(2048);
-
- string header = WebEncoders.Base64UrlEncode("fake header"u8.ToArray());
- string payload = WebEncoders.Base64UrlEncode("fake payload"u8.ToArray());
- var jwtString = JwtSignatureGenerator.GenerateCompleteJwt(header, payload, rsa);
-
- Assert.True(ValidateJwtSignature(jwtString, rsa));
- }
-
- ///
- /// This test is to prove our signature verification code is correct. The inputs are
- /// all from a production keycloak instance.
- ///
- [Fact]
- public void ValidateKeycloakSignature()
- {
- string keycloakGeneratedJwt =
- "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJybVZ3TU5rM0o1WHlmMWhyS3NVbEVYN1BNUm42dlZKY0h3U3FYMUVQRnFJIn0.eyJleHAiOjE3NzEwNTQxMDksImlhdCI6MTc3MTA1MzgwOSwiYXV0aF90aW1lIjoxNzcxMDUzODA4LCJqdGkiOiI5MTEzZjEwNC03YzllLTQzNzItYmU4Yy03NDMwMmI1ZTU1NGUiLCJpc3MiOiJodHRwczovL2lhbS5rYXNzYWNsb3VkLm5sL2F1dGgvcmVhbG1zL21wbHVza2Fzc2EiLCJhdWQiOlsia2Fzc2EtbWFuYWdlbWVudC1zZXJ2aWNlIiwiYXBhY2hlMi1pbnRyYW5ldC1hdXRoIiwiYWNjb3VudCJdLCJzdWIiOiIwOTNjY2YxNS1jNGE5LTRhYjQtOTcxZi1kNWEwMjIzNmQ4NWEiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJkZWFsZXJfc3VwcG9ydCIsInNpZCI6IjRiYjI0OGQ1LWVkNzktNGU0Yy05NWNjLTAwYzgzMzliZmZiMyIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOlsiaHR0cHM6Ly9tcGx1c2thc3NhLm9ubGluZSIsImh0dHBzOi8vd3d3Lm1wbHVza2Fzc2Euc3VwcG9ydCIsImh0dHBzOi8vbXBsdXNrYXNzYS5zdXBwb3J0IiwiaHR0cDovL2xvY2FsaG9zdDo0MDkwIiwiaHR0cHM6Ly93d3cubXBsdXNrYXNzYS5vbmxpbmUiLCJodHRwOi8vbG9jYWxob3N0IiwiLyoiLCJodHRwOi8vbG9jYWxob3N0OjQyMDAiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImRlZmF1bHQtcm9sZXMtbXBsdXNrYXNzYSIsIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iLCJkZWFsZXItbWVkZXdlcmtlci1yb2xlIiwibXBsdXNrYXNzYS1tZWRld2Vya2VyLXJvbGUiXX0sInJlc291cmNlX2FjY2VzcyI6eyJhcGFjaGUyLWludHJhbmV0LWF1dGgiOnsicm9sZXMiOlsiaW50cmFuZXQiLCJyZWxlYXNlbm90ZXNfd3JpdGUiXX0sImthc3NhLW1hbmFnZW1lbnQtc2VydmljZSI6eyJyb2xlcyI6WyJwb3NhY2NvdW50X3Bhc3N3b3JkcmVzZXQiLCJkcmFmdF9saWNlbnNlX3dyaXRlIiwibGljZW5zZV9yZWFkIiwia25vd2xlZGdlSXRlbV9yZWFkIiwibWFpbGluZ19yZWFkIiwibXBsdXNhcGlfcmVhZCIsImRhdGFiYXNlX3VzZXJfd3JpdGUiLCJlbnZpcm9ubWVudF93cml0ZSIsImdrc19hdXRoY29kZV9yZWFkIiwiZW1wbG95ZWVfcmVhZCIsImRhdGFiYXNlX3VzZXJfcmVhZCIsImFwaWFjY291bnRfcGFzc3dvcmRyZXNldCIsIm1wbHVzYXBpX3dyaXRlIiwiZW52aXJvbm1lbnRfcmVhZCIsImtub3dsZWRnZUl0ZW1fd3JpdGUiLCJkYXRhYmFzZV91c2VyX3Bhc3N3b3JkX3JlYWQiLCJsaWNlbnNlX3dyaXRlIiwiY3VzdG9tZXJfd3JpdGUiLCJkZWFsZXJfcmVhZCIsImVtcGxveWVlX3dyaXRlIiwiZGF0YWJhc2VfY29uZmlndXJhdGlvbl93cml0ZSIsInJlbGF0aW9uc19yZWFkIiwiZGF0YWJhc2VfdXNlcl9wYXNzd29yZF9tcGx1c19lbmNyeXB0ZWRfcmVhZCIsImRyYWZ0X2xpY2Vuc2VfcmVhZCIsImRhdGFiYXNlX2NvbmZpZ3VyYXRpb25fcmVhZCJdfSwiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJvcGVuaWQga21zIGVtYWlsIHByb2ZpbGUiLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiZGVhbGVySWQiOjEsIm5hbWUiOiJFZWxrZSBLbGVpbiIsInByZWZlcnJlZF91c2VybmFtZSI6ImVlbGtlQGJvbHQubmwiLCJsb2NhbGUiOiJlbiIsImdpdmVuX25hbWUiOiJFZWxrZSIsImZhbWlseV9uYW1lIjoiS2xlaW4iLCJlbWFpbCI6ImVlbGtlQGJvbHQubmwiLCJlbXBsb3llZU51bWJlciI6NTR9.SHjVTWsFwiaKTxBX-0GZM1pK8rOodkYnEu_QJ4dlPpozai9j3RRJK3DswsuEbJC8PdQXI4-AI0-5JGBQi2gDXdFSVHhAblnmjva0sWCaY7lG2ASa65UKM_4RzH-6nvQ9EiZXdANzsWkLG350l-dLiqdt--Lpjpw2huK_GKAx20SKfauKBmm990rHzrl0Uii3wQ3fPHlAJ_8-WSnSBquOH8xsYJHa1LOsc2WqbEDnMA4hRnGvCoubwhkOANfWSx0OCwSIKBddrcts64ZAxFhmilZXGzWMqDkblY2fDU8_jrlysgYsymQlOVwwg7V5Ps-DJkGXWvmpncKfyYd3Vuwusg";
- string keycloakKeySet = """
- {
- "keys": [
- {
- "kid": "rmVwMNk3J5Xyf1hrKsUlEX7PMRn6vVJcHwSqX1EPFqI",
- "kty": "RSA",
- "alg": "RS256",
- "use": "sig",
- "n": "pYbLAeOLDEwzL4tEwuE2LfisOBXoQqWA9RdP3ph6muwF1ErfhiBSIB2JETKf7F1OsiF1_qnuh4uDfn0TO8bK3lSfHTlIHWShwaJ_UegS9ylobfIYXJsz0xmJK5ToFaSYa72D_Dyln7ROxudu8-zc70sz7bUKQ0_ktWRsiu76vY6Kr9-18PgaooPmb2QP8lS8IZEv-gW5SLqoMc1DfD8lsih1sdnQ8W65cBsNnenkWc97AF9cMR6rdD2tZfLAxEHKYaohAL9EsQsLic3P2f2UaqRTAOvgqyYE5hyJROt7Pyeyi8YSy7zXD12h2mc0mrSoA-u7s_GrOLcLoLLgEnRRVw",
- "e": "AQAB",
- "x5c": [
- "MIICozCCAYsCBgFwfLC07DANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDDAptcGx1c2thc3NhMB4XDTIwMDIyNTE0MTAyMFoXDTMwMDIyNTE0MTIwMFowFTETMBEGA1UEAwwKbXBsdXNrYXNzYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKWGywHjiwxMMy+LRMLhNi34rDgV6EKlgPUXT96YeprsBdRK34YgUiAdiREyn+xdTrIhdf6p7oeLg359EzvGyt5Unx05SB1kocGif1HoEvcpaG3yGFybM9MZiSuU6BWkmGu9g/w8pZ+0TsbnbvPs3O9LM+21CkNP5LVkbIru+r2Oiq/ftfD4GqKD5m9kD/JUvCGRL/oFuUi6qDHNQ3w/JbIodbHZ0PFuuXAbDZ3p5FnPewBfXDEeq3Q9rWXywMRBymGqIQC/RLELC4nNz9n9lGqkUwDr4KsmBOYciUTrez8nsovGEsu81w9dodpnNJq0qAPru7Pxqzi3C6Cy4BJ0UVcCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAYcJVYv8HzZQIMrqhIyu7EVihPAx0w9NaZ1xzB9qCHrwie6ZLQdnMm8l0IdehyYuY+0HK7FjC8dAcT4nklOQDg4iCp7ZrM7vFNP+60pR0i7aIbf0cFXy9VTOPvUsXmu+p1LqRQLRJD0BjO29gupTe68KyTtyuX5A7JfmCq84j5i45Md8A9MAMZWnXMSHiaZLtlOS/4t4cdc371uq9fH7SKusnvUY0d14+BzcrHi/eurhKUUjJZ1xclUE2trOXFrE78fSMUmeGbIlpAV0MtrJW7OmXmaHH8Q1wTm78RH/dn4EmWSoFcigdVcDge941/MT2soDSIGLUnYiYrW8d6HE2Lg=="
- ],
- "x5t": "rj9_q26MIdowvyJJbyHySeUl1y8",
- "x5t#S256": "KNyQ8ngE925F__ZPJm-wCNUnGBJQGJbZGGjlCvmwBkM"
- }
- ]
- }
- """;
- JsonWebKeySet keySet = JsonSerializer.Deserialize(keycloakKeySet)!;
- using RSA publicKey = LoadFromJwk(keySet.Keys[0]);
-
- Assert.True(ValidateJwtSignature(keycloakGeneratedJwt, publicKey));
- }
-
- private bool ValidateJwtSignature(string jwtString, RSA publicKey)
- {
- int lastDotIndex = jwtString.LastIndexOf('.');
-
- return publicKey.VerifyData(
- Encoding.UTF8.GetBytes(jwtString, 0, lastDotIndex),
- WebEncoders.Base64UrlDecode(jwtString, lastDotIndex + 1, jwtString.Length - (lastDotIndex + 1)),
- HashAlgorithmName.SHA256,
- RSASignaturePadding.Pkcs1);
- }
-
- private static RSA LoadFromJwk(JsonWebKey jwk)
- {
- var rsa = RSA.Create();
- var parameters = new RSAParameters
- {
- Modulus = WebEncoders.Base64UrlDecode(jwk.Modulus!),
- Exponent = WebEncoders.Base64UrlDecode(jwk.Exponent!)
- };
-
- rsa.ImportParameters(parameters);
- return rsa;
- }
-
-}
\ No newline at end of file
diff --git a/IdentityShroud.Core.Tests/Model/RealmTests.cs b/IdentityShroud.Core.Tests/Model/RealmTests.cs
new file mode 100644
index 0000000..959d8b1
--- /dev/null
+++ b/IdentityShroud.Core.Tests/Model/RealmTests.cs
@@ -0,0 +1,51 @@
+using IdentityShroud.Core.Contracts;
+using IdentityShroud.Core.Model;
+
+namespace IdentityShroud.Core.Tests.Model;
+
+public class RealmTests
+{
+ [Fact]
+ public void SetNewKey()
+ {
+ byte[] privateKey = [5, 6, 7, 8];
+ byte[] encryptedPrivateKey = [1, 2, 3, 4];
+
+ var encryptionService = Substitute.For();
+ encryptionService
+ .Encrypt(Arg.Any())
+ .Returns(x => encryptedPrivateKey);
+
+ Realm realm = new();
+ realm.SetPrivateKey(encryptionService, privateKey);
+
+ // should be able to return original without calling decrypt
+ Assert.Equal(privateKey, realm.GetPrivateKey(encryptionService));
+ Assert.Equal(encryptedPrivateKey, realm.PrivateKeyEncrypted);
+
+ encryptionService.Received(1).Encrypt(privateKey);
+ encryptionService.DidNotReceive().Decrypt(Arg.Any());
+ }
+
+ [Fact]
+ public void GetDecryptedKey()
+ {
+ byte[] privateKey = [5, 6, 7, 8];
+ byte[] encryptedPrivateKey = [1, 2, 3, 4];
+
+ var encryptionService = Substitute.For();
+ encryptionService
+ .Decrypt(encryptedPrivateKey)
+ .Returns(x => privateKey);
+
+ Realm realm = new();
+ realm.PrivateKeyEncrypted = encryptedPrivateKey;
+
+ // should be able to return original without calling decrypt
+ Assert.Equal(privateKey, realm.GetPrivateKey(encryptionService));
+ Assert.Equal(encryptedPrivateKey, realm.PrivateKeyEncrypted);
+
+ encryptionService.Received(1).Decrypt(encryptedPrivateKey);
+ }
+
+}
\ No newline at end of file
diff --git a/IdentityShroud.Core.Tests/Security/AesGcmHelperTests.cs b/IdentityShroud.Core.Tests/Security/AesGcmHelperTests.cs
new file mode 100644
index 0000000..6392676
--- /dev/null
+++ b/IdentityShroud.Core.Tests/Security/AesGcmHelperTests.cs
@@ -0,0 +1,21 @@
+using System.Security.Cryptography;
+using System.Text;
+using IdentityShroud.Core.Security;
+
+namespace IdentityShroud.Core.Tests.Security;
+
+public class AesGcmHelperTests
+{
+ [Fact]
+ public void EncryptDecryptCycleWorks()
+ {
+ string input = "Hello, world!";
+
+ var encryptionKey = RandomNumberGenerator.GetBytes(32);
+
+ var cypher = AesGcmHelper.EncryptAesGcm(Encoding.UTF8.GetBytes(input), encryptionKey);
+ var output = AesGcmHelper.DecryptAesGcm(cypher, encryptionKey);
+
+ Assert.Equal(input, Encoding.UTF8.GetString(output));
+ }
+}
\ No newline at end of file
diff --git a/IdentityShroud.Core.Tests/Security/ConfigurationSecretProviderTests.cs b/IdentityShroud.Core.Tests/Security/ConfigurationSecretProviderTests.cs
deleted file mode 100644
index 01851a4..0000000
--- a/IdentityShroud.Core.Tests/Security/ConfigurationSecretProviderTests.cs
+++ /dev/null
@@ -1,63 +0,0 @@
-using System.Text;
-using IdentityShroud.Core.Security;
-using Microsoft.Extensions.Configuration;
-
-namespace IdentityShroud.Core.Tests.Security;
-
-public class ConfigurationSecretProviderTests
-{
- private static IConfiguration BuildConfigFromJson(string json)
- {
- // Convert the JSON string into a stream that the config builder can read.
- var jsonBytes = Encoding.UTF8.GetBytes(json);
- using var stream = new MemoryStream(jsonBytes);
-
- // Build the configuration just like the real app does, but from the stream.
- var config = new ConfigurationBuilder()
- .AddJsonStream(stream) // <-- reads from the in‑memory JSON
- .Build();
-
- return config;
- }
-
- [Fact]
- public void Test()
- {
- string jsonConfig = """
- {
- "secrets": {
- "master": [
- {
- "Id": "5676d159-5495-4945-aa84-59ee694aa8a2",
- "Active": true,
- "Algorithm": "AES",
- "Key": "yoQ4W7EaNjo7s3FBYkWo5BLyX1BnLyWd7BlSaDIrkzo="
- },
- {
- "Id": "b82489e7-a05a-4d64-b9a5-58d2f2c0dc39",
- "Active": false,
- "Algorithm": "AES",
- "Key": "YSWK6vTJXCJOGLpCo+TtZ6anKNzvA1VT2xXLHbmq4M0="
- }
- ]
- }
- }
- """;
-
-
- ConfigurationSecretProvider sut = new(BuildConfigFromJson(jsonConfig));
-
- // act
- var keys = sut.GetKeys("master");
-
- // verify
- Assert.Equal(2, keys.Length);
- var active = keys.Single(k => k.Active);
- Assert.Equal(new Guid("5676d159-5495-4945-aa84-59ee694aa8a2"), active.Id.Id);
- Assert.Equal("AES", active.Algorithm);
- Assert.Equal(Convert.FromBase64String("yoQ4W7EaNjo7s3FBYkWo5BLyX1BnLyWd7BlSaDIrkzo="), active.Key);
-
- var inactive = keys.Single(k => !k.Active);
- Assert.Equal(new Guid("b82489e7-a05a-4d64-b9a5-58d2f2c0dc39"), inactive.Id.Id);
- }
-}
\ No newline at end of file
diff --git a/IdentityShroud.Core.Tests/Security/Jwt/RsaJwtSignerTests.cs b/IdentityShroud.Core.Tests/Security/Jwt/RsaJwtSignerTests.cs
deleted file mode 100644
index 13b76ac..0000000
--- a/IdentityShroud.Core.Tests/Security/Jwt/RsaJwtSignerTests.cs
+++ /dev/null
@@ -1,48 +0,0 @@
-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;
-
-namespace IdentityShroud.Core.Tests.Security.Jwt;
-
-public class RsaJwtSignerTests
-{
- [Fact]
- public void Test()
- {
- // ISecretProvider secretProvider = Substitute.For();
- // RealmSigningKey privateKey = new()
- // {
- // Id = default,
- // KeyType = KeyType.RSA,
- // Key = new EncryptedDek(KekId.NewId(), [1]),
- // CreatedAt = default,
- // RevokedAt = null,
- // Priority = 0,
- // PublicKeyParameters = null
- // };
- DecryptedSigningKey key = new();
- byte[] jwt = [];
-
- RsaJwtSigner provider = new();
- provider.CalculateSignature(JwtSigAlgName.RS256, key, jwt);
- //
- // new DekEncryptionService(secretProvider), privateKey,
- // JwtSigAlgName.RS256);
- }
-
- [Theory]
- [InlineData(1024)]
- [InlineData(2048)]
- [InlineData(4096)]
- public void EstimateKeySizeTests(int keySizeBits)
- {
- using var rsa = RSA.Create();
- rsa.KeySize = keySizeBits;
- byte[] b = rsa.ExportPkcs8PrivateKey();
- int estimate = DecryptedSigningKey.EstimatePkcs8ExportSize(keySizeBits);
- Assert.True(b.Length < estimate - 100);
- }
-}
\ No newline at end of file
diff --git a/IdentityShroud.Core.Tests/Services/ClientServiceTests.cs b/IdentityShroud.Core.Tests/Services/ClientServiceTests.cs
deleted file mode 100644
index a0690c9..0000000
--- a/IdentityShroud.Core.Tests/Services/ClientServiceTests.cs
+++ /dev/null
@@ -1,193 +0,0 @@
-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;
-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
-{
- private readonly DbFixture _dbFixture;
- private readonly NullDataEncryptionService _dataEncryptionService = new();
-
- private readonly IClock _clock = Substitute.For();
- private readonly Guid _realmId = new("a1b2c3d4-0000-0000-0000-000000000001");
-
- public ClientServiceTests(DbFixture dbFixture)
- {
- _dbFixture = dbFixture;
- using Db db = dbFixture.CreateDbContext();
- if (!db.Database.EnsureCreated())
- TruncateTables(db);
- EnsureRealm(db);
- }
-
- private void TruncateTables(Db db)
- {
- db.Database.ExecuteSqlRaw("TRUNCATE client CASCADE;");
- db.Database.ExecuteSqlRaw("TRUNCATE realm CASCADE;");
- }
-
- private void EnsureRealm(Db db)
- {
- if (!db.Realms.Any(r => r.Id == _realmId))
- {
- 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 withSecret)
- {
- // Setup
- DateTime now = DateTime.UtcNow;
- _clock.UtcNow().Returns(now);
-
- Client val;
- await using (var db = _dbFixture.CreateDbContext())
- {
- // Act
- ClientService sut = CreateSut(db);
- var response = await sut.Create(
- _realmId,
- ClientCreateRequestBuilder.Default() with
- {
- Confidential = withSecret,
- GenerateSecret = withSecret,
- },
- TestContext.Current.CancellationToken);
-
- // Verify
- val = ResultAssert.Success(response);
- Assert.Equal(_realmId, val.RealmId);
- Assert.Equal("test-client", val.ClientId);
- Assert.Equal("Test Client", val.Name);
- Assert.Equal("A test client", val.Description);
- Assert.Equal(withSecret, val.Confidential);
- Assert.Equal(now, val.CreatedAt);
- }
-
- await using (var db = _dbFixture.CreateDbContext())
- {
- var dbRecord = await db.Clients
- .Include(e => e.Secrets)
- .SingleAsync(e => e.Id == val.Id, TestContext.Current.CancellationToken);
-
- if (withSecret)
- Assert.Single(dbRecord.Secrets);
- else
- Assert.Empty(dbRecord.Secrets);
- }
- }
-
- [Theory]
- [InlineData("existing-client", true)]
- [InlineData("missing-client", false)]
- public async Task GetByClientId(string clientId, bool shouldFind)
- {
- // Setup
- _clock.UtcNow().Returns(DateTime.UtcNow);
- await using (var setupContext = _dbFixture.CreateDbContext())
- {
- setupContext.Clients.Add(new()
- {
- RealmId = _realmId,
- ClientId = "existing-client",
- CreatedAt = DateTime.UtcNow,
- });
-
- await setupContext.SaveChangesAsync(TestContext.Current.CancellationToken);
- }
-
- await using var actContext = _dbFixture.CreateDbContext();
- // Act
- ClientService sut = CreateSut(actContext);
- Client? result = await sut.GetByClientId(_realmId, clientId, TestContext.Current.CancellationToken);
-
- // Verify
- if (shouldFind)
- Assert.NotNull(result);
- else
- Assert.Null(result);
- }
-
- [Theory]
- [InlineData(true)]
- [InlineData(false)]
- public async Task FindById(bool shouldFind)
- {
- // Setup
- _clock.UtcNow().Returns(DateTime.UtcNow);
- int existingId;
- await using (var setupContext = _dbFixture.CreateDbContext())
- {
- Client client = new()
- {
- RealmId = _realmId,
- ClientId = "find-by-id-client",
- CreatedAt = DateTime.UtcNow,
- };
- setupContext.Clients.Add(client);
- await setupContext.SaveChangesAsync(TestContext.Current.CancellationToken);
- existingId = client.Id;
- }
-
- int searchId = shouldFind ? existingId : existingId + 9999;
-
- await using var actContext = _dbFixture.CreateDbContext();
- // Act
- ClientService sut = CreateSut(actContext);
- Client? result = await sut.FindById(_realmId, searchId, TestContext.Current.CancellationToken);
-
- // Verify
- if (shouldFind)
- Assert.NotNull(result);
- else
- Assert.Null(result);
- }
-}
diff --git a/IdentityShroud.Core.Tests/Services/DataEncryptionServiceTests.cs b/IdentityShroud.Core.Tests/Services/DataEncryptionServiceTests.cs
deleted file mode 100644
index a61e7e0..0000000
--- a/IdentityShroud.Core.Tests/Services/DataEncryptionServiceTests.cs
+++ /dev/null
@@ -1,60 +0,0 @@
-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;
-
-namespace IdentityShroud.Core.Tests.Services;
-
-public class DataEncryptionServiceTests
-{
-// private readonly IRealmContext _realmContext = Substitute.For();
- private readonly IDekEncryptionService _dekCryptor = new NullDekEncryptionService();// Substitute.For();
-
- private readonly DekId _activeDekId = DekId.NewId();
- private readonly DekId _secondDekId = DekId.NewId();
- private DataEncryptionService CreateSut()
- => new(_dekCryptor);
-
- [Fact]
- public void Encrypt_UsesActiveKey()
- {
- var dek = CreateRealmDek(_activeDekId, true);
-
- var cipher = CreateSut().Encrypt(dek, "Hello"u8);
-
- Assert.Equal(_activeDekId, cipher.DekId);
- }
-
- [Fact]
- public void Decrypt_UsesCorrectKey()
- {
- var first = CreateRealmDek(_activeDekId, true);
-
- var sut = CreateSut();
- var cipher = sut.Encrypt(first, "Hello"u8);
-
- // Deactivate original key
- first.Active = false;
- // Make new active
- var second = CreateRealmDek(_secondDekId, true);
- // Return both
- RealmDek[] list = [ first, second ];
-
- var decoded = sut.Decrypt(list, cipher);
-
- Assert.Equal("Hello"u8, decoded);
- }
-
- private RealmDek CreateRealmDek(DekId id, bool active)
- => new()
- {
- Id = id,
- Active = active,
- Algorithm = KeyType.AES,
- KeyData = new(KekId.NewId(), RandomNumberGenerator.GetBytes(32)),
- RealmId = default,
- };
-}
\ No newline at end of file
diff --git a/IdentityShroud.Core.Tests/Services/DekEncryptionServiceTests.cs b/IdentityShroud.Core.Tests/Services/DekEncryptionServiceTests.cs
deleted file mode 100644
index c0b9f38..0000000
--- a/IdentityShroud.Core.Tests/Services/DekEncryptionServiceTests.cs
+++ /dev/null
@@ -1,131 +0,0 @@
-using IdentityShroud.Core.Contracts;
-using IdentityShroud.Core.Security;
-using IdentityShroud.Core.Services;
-
-namespace IdentityShroud.Core.Tests.Services;
-
-public class DekEncryptionServiceTests
-{
- [Fact]
- public void RoundtripWorks()
- {
- // Note this code will tend to only test the latest verion.
-
- // setup
- byte[] keyValue = Convert.FromBase64String("IGd9yUMusjNW0ezv8ink3QWlAHKFH45d21LyrbJTokw=");
- var secretProvider = Substitute.For();
- KeyEncryptionKey[] keys =
- [
- new KeyEncryptionKey(KekId.NewId(), true, "AES", keyValue)
- ];
- secretProvider.GetKeys("master").Returns(keys);
-
-
- ReadOnlySpan input = "Hello, World!"u8;
-
- // act
- DekEncryptionService sut = new(secretProvider);
-
- EncryptedDek cipher = sut.Encrypt(input.ToArray());
- int decryptedSize = sut.GetDecryptedSize(cipher);
- Assert.Equal(input.Length, decryptedSize);
-
- var result = new byte[decryptedSize];
- sut.Decrypt(cipher, result);
-
- // verify
- Assert.Equal(input, result);
- }
-
- [Fact]
- public void DetectsCorruptInput()
- {
- // When introducing a new version we need version specific tests to
- // make sure decoding of legacy data still works.
- KekId kid = KekId.NewId();
- // setup
- byte[] cipher = // NOTE INCORRECT CIPHER DO NOT USE IN OTHER TESTS
- [
- 1, 198, 55, 58, 56, 110, 238, 59, 158, 214, 85, 241, 26, 44, 140, 229, 128, 111, 167, 154, 160, 177, 152,
- 193, 75, 4, 235, 82, 207, 87, 32, 10, 239, 4, 246, 25, 21, 249, 25, 59, 160, 101
- ];
- EncryptedDek secret = new(kid, cipher);
-
- byte[] keyValue = Convert.FromBase64String("IGd9yUMusjNW0ezv8ink3QWlAHKFH45d21LyrbJTokw=");
- var secretProvider = Substitute.For();
- KeyEncryptionKey[] keys =
- [
- new KeyEncryptionKey(kid, true, "AES", keyValue)
- ];
- secretProvider.GetKeys("master").Returns(keys);
-
- // act
- DekEncryptionService sut = new(secretProvider);
- int decryptedSize = sut.GetDecryptedSize(secret);
- var result = new byte[decryptedSize];
- Assert.Throws(
- () => sut.Decrypt(secret, result),
- ex => ex.Message.Contains("Decryption failed") ? null : "Expected Decryption failed in message");
- }
-
- [Fact]
- public void DecodeSelectsRightKey()
- {
- // The key is marked inactive also it is the second key
-
- // setup
- KekId kid1 = KekId.NewId();
- KekId kid2 = KekId.NewId();
-
- byte[] cipher =
- [
- 1, 198, 55, 58, 56, 110, 238, 59, 158, 214, 85, 241, 26, 44, 140, 229, 128, 111, 167, 154, 160, 177, 152,
- 193, 74, 4, 235, 82, 207, 87, 32, 10, 239, 4, 246, 25, 21, 249, 25, 59, 160, 101
- ];
- EncryptedDek secret = new(kid1, cipher);
-
- byte[] keyValue1 = Convert.FromBase64String("IGd9yUMusjNW0ezv8ink3QWlAHKFH45d21LyrbJTokw=");
- byte[] keyValue2 = Convert.FromBase64String("Dat1RwRvuLX3wdKMMP4NwHdBl8tJJsKfp01qikyo8aw=");
- var secretProvider = Substitute.For();
- KeyEncryptionKey[] keys =
- [
- new KeyEncryptionKey(kid2, true, "AES", keyValue2),
- new KeyEncryptionKey(kid1, false, "AES", keyValue1),
- ];
- secretProvider.GetKeys("master").Returns(keys);
-
- // act
- DekEncryptionService sut = new(secretProvider);
- byte[] result = new byte[sut.GetDecryptedSize(secret)];
- sut.Decrypt(secret, result);
-
- // verify
- Assert.Equal("Hello, World!"u8, result);
- }
-
- [Fact]
- public void EncryptionUsesActiveKey()
- {
- // setup
- KekId kid1 = KekId.NewId();
- KekId kid2 = KekId.NewId();
-
- byte[] keyValue1 = Convert.FromBase64String("IGd9yUMusjNW0ezv8ink3QWlAHKFH45d21LyrbJTokw=");
- byte[] keyValue2 = Convert.FromBase64String("Dat1RwRvuLX3wdKMMP4NwHdBl8tJJsKfp01qikyo8aw=");
- var secretProvider = Substitute.For();
- KeyEncryptionKey[] keys =
- [
- new KeyEncryptionKey(kid1, false, "AES", keyValue1),
- new KeyEncryptionKey(kid2, true, "AES", keyValue2),
- ];
- secretProvider.GetKeys("master").Returns(keys);
-
- ReadOnlySpan input = "Hello, World!"u8;
- // act
- DekEncryptionService sut = new(secretProvider);
- EncryptedDek cipher = sut.Encrypt(input.ToArray());
-
- // Verify
- Assert.Equal(kid2, cipher.KekId);
- }
-}
\ No newline at end of file
diff --git a/IdentityShroud.Core.Tests/Services/EncryptionServiceTests.cs b/IdentityShroud.Core.Tests/Services/EncryptionServiceTests.cs
new file mode 100644
index 0000000..e97b2df
--- /dev/null
+++ b/IdentityShroud.Core.Tests/Services/EncryptionServiceTests.cs
@@ -0,0 +1,22 @@
+using System.Security.Cryptography;
+using IdentityShroud.Core.Services;
+
+namespace IdentityShroud.Core.Tests.Services;
+
+public class EncryptionServiceTests
+{
+ [Fact]
+ public void RoundtripWorks()
+ {
+ // setup
+ string key = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));
+ EncryptionService sut = new(key);
+ byte[] input = RandomNumberGenerator.GetBytes(16);
+
+ // act
+ var cipher = sut.Encrypt(input);
+ var result = sut.Decrypt(cipher);
+
+ Assert.Equal(input, result);
+ }
+}
\ No newline at end of file
diff --git a/IdentityShroud.Core.Tests/Services/EncryptionTests.cs b/IdentityShroud.Core.Tests/Services/EncryptionTests.cs
deleted file mode 100644
index 32e4538..0000000
--- a/IdentityShroud.Core.Tests/Services/EncryptionTests.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-using IdentityShroud.Core.Security;
-
-namespace IdentityShroud.Core.Tests.Services;
-
-public class EncryptionTests
-{
- [Fact]
- public void DecodeV1_Success()
- {
- // When introducing a new version we need version specific tests to
- // make sure decoding of legacy data still works.
-
- // setup
- byte[] cipher =
- [
- 1, 198, 55, 58, 56, 110, 238, 59, 158, 214, 85, 241, 26, 44, 140, 229, 128, 111, 167, 154, 160, 177, 152,
- 193, 74, 4, 235, 82, 207, 87, 32, 10, 239, 4, 246, 25, 21, 249, 25, 59, 160, 101
- ];
- byte[] keyValue = Convert.FromBase64String("IGd9yUMusjNW0ezv8ink3QWlAHKFH45d21LyrbJTokw=");
-
- // act
- byte[] result = new byte[Encryption.GetDecryptedLength(cipher)];
- Encryption.Decrypt(cipher, keyValue, result);
-
- // verify
- Assert.Equal("Hello, World!"u8, result);
- }
-
-
-}
\ No newline at end of file
diff --git a/IdentityShroud.Core.Tests/Services/RealmServiceTests.cs b/IdentityShroud.Core.Tests/Services/RealmServiceTests.cs
index 70d6d11..0ad00ef 100644
--- a/IdentityShroud.Core.Tests/Services/RealmServiceTests.cs
+++ b/IdentityShroud.Core.Tests/Services/RealmServiceTests.cs
@@ -1,148 +1,53 @@
using FluentResults;
-using IdentityShroud.Core.Contracts;
-using IdentityShroud.Core.EFCore;
-using IdentityShroud.Core.Model;
-using IdentityShroud.Core.Security.Keys;
using IdentityShroud.Core.Services;
using IdentityShroud.Core.Tests.Fixtures;
-using IdentityShroud.TestUtils.Substitutes;
+using IdentityShroud.Core.Tests.Substitutes;
using Microsoft.EntityFrameworkCore;
-using Shouldly;
namespace IdentityShroud.Core.Tests.Services;
public class RealmServiceTests : IClassFixture
{
- private readonly DbFixture _dbFixture;
- private readonly IKeyService _keyService = Substitute.For();
- private readonly IDekEncryptionService _dekCryptor = new NullDekEncryptionService();
+ private readonly Db _db;
public RealmServiceTests(DbFixture dbFixture)
{
- _dbFixture = dbFixture;
- using Db db = dbFixture.CreateDbContext();
- if (!db.Database.EnsureCreated())
- TruncateTables(db);
+ _db = dbFixture.CreateDbContext("realmservice");
+
+ if (!_db.Database.EnsureCreated())
+ TruncateTables();
}
- private void TruncateTables(Db db)
+ private void TruncateTables()
{
- db.Database.ExecuteSqlRaw("TRUNCATE realm CASCADE;");
+ _db.Database.ExecuteSqlRaw("TRUNCATE realm CASCADE;");
}
-
- private RealmService CreateSut(Db db) => new(db, _keyService, _dekCryptor, new ClockService());
-
[Theory]
[InlineData(null)]
[InlineData("a7c2a39c-3ed9-4790-826e-43bb2e5e480c")]
public async Task Create(string? idString)
{
- // Setup
Guid? realmId = null;
if (idString is not null)
realmId = new(idString);
-
- Realm? val;
- await using (var db = _dbFixture.CreateDbContext())
- {
- _keyService.CreateKey(Arg.Any())
- .Returns(new CreateKeyResponse(KeyType.AES, new KeyData([21])));
- // Act
- RealmService sut = CreateSut(db);
- Result response = await sut.Create(
- new(realmId, "slug", "New realm"),
- TestContext.Current.CancellationToken);
-
- // Verify
- val = ResultAssert.Success(response);
- if (realmId.HasValue)
- Assert.Equal(realmId, val.Id);
- else
- Assert.NotEqual(Guid.Empty, val.Id);
-
- 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());
- }
-
- await using (var db = _dbFixture.CreateDbContext())
- {
- var dbRecord = await db.Realms
- .Include(e => e.TokenSigningKeys)
- .SingleAsync(e => e.Id == val.Id, TestContext.Current.CancellationToken);
- Assert.Equal(KeyType.AES, dbRecord.TokenSigningKeys[0].KeyType);
- }
- }
-
- [Theory]
- [InlineData("slug", null)]
- [InlineData("foo", "Foo")]
- public async Task FindBySlug(string slug, string? name)
- {
- await using (var setupContext = _dbFixture.CreateDbContext())
- {
- setupContext.Realms.Add(new()
- {
- Slug = "foo",
- Name = "Foo",
- });
- setupContext.Realms.Add(new()
- {
- Slug = "bar",
- Name = "Bar",
- });
-
- await setupContext.SaveChangesAsync(TestContext.Current.CancellationToken);
- }
-
- await using var actContext = _dbFixture.CreateDbContext();
- // Act
- RealmService sut = CreateSut(actContext);
- var result = await sut.FindBySlug(slug, TestContext.Current.CancellationToken);
-
- // Verify
- Assert.Equal(name, result?.Name);
- }
-
- [Theory]
- [InlineData("b0423bba-2411-497b-a5b6-c5adf404b862", true)]
- [InlineData("65ac9dba-6d43-4fa4-b57f-133ed639fbcb", false)]
- public async Task FindById(string idString, bool shouldFind)
- {
- Guid id = new(idString);
- await using (var setupContext = _dbFixture.CreateDbContext())
- {
- setupContext.Realms.Add(new()
- {
- Id = new("b0423bba-2411-497b-a5b6-c5adf404b862"),
- Slug = "foo",
- Name = "Foo",
- });
- setupContext.Realms.Add(new()
- {
- Id = new("d4ffc7d0-7b2c-4f02-82b9-a74610435b0d"),
- Slug = "bar",
- Name = "Bar",
- });
-
- await setupContext.SaveChangesAsync(TestContext.Current.CancellationToken);
- }
-
- await using var actContext = _dbFixture.CreateDbContext();
- // Act
- RealmService sut = CreateSut(actContext);
- Realm? result = await sut.FindById(id, TestContext.Current.CancellationToken);
-
- // Verify
- if (shouldFind)
- Assert.NotNull(result);
- else
- Assert.Null(result);
+
+ var encryptionService = EncryptionServiceSubstitute.CreatePassthrough();
+ RealmService sut = new(_db, encryptionService);
+
+ var response = await sut.Create(
+ new(realmId, "slug", "New realm"),
+ TestContext.Current.CancellationToken);
+
+ RealmCreateResponse val = ResultAssert.Success(response);
+ if (realmId.HasValue)
+ Assert.Equal(realmId, val.Id);
+ else
+ Assert.NotEqual(Guid.Empty, val.Id);
+
+ Assert.Equal("slug", val.Slug);
+ Assert.Equal("New realm", val.Name);
+
+ // TODO verify data has been stored!
}
}
\ No newline at end of file
diff --git a/IdentityShroud.Core.Tests/Substitutes/EncryptionServiceSubstitute.cs b/IdentityShroud.Core.Tests/Substitutes/EncryptionServiceSubstitute.cs
new file mode 100644
index 0000000..cf79318
--- /dev/null
+++ b/IdentityShroud.Core.Tests/Substitutes/EncryptionServiceSubstitute.cs
@@ -0,0 +1,18 @@
+using IdentityShroud.Core.Contracts;
+
+namespace IdentityShroud.Core.Tests.Substitutes;
+
+public static class EncryptionServiceSubstitute
+{
+ public static IEncryptionService CreatePassthrough()
+ {
+ var encryptionService = Substitute.For();
+ encryptionService
+ .Encrypt(Arg.Any())
+ .Returns(x => x.ArgAt(0));
+ encryptionService
+ .Decrypt(Arg.Any())
+ .Returns(x => x.ArgAt(0));
+ return encryptionService;
+ }
+}
\ No newline at end of file
diff --git a/IdentityShroud.Core.Tests/UnitTest1.cs b/IdentityShroud.Core.Tests/UnitTest1.cs
index 7cfc961..a998f4a 100644
--- a/IdentityShroud.Core.Tests/UnitTest1.cs
+++ b/IdentityShroud.Core.Tests/UnitTest1.cs
@@ -1,7 +1,8 @@
-using System.Buffers.Text;
-using System.Security.Cryptography;
+using System.Security.Cryptography;
+using System.Text;
using System.Text.Json;
-using IdentityShroud.Core.DTO;
+using IdentityShroud.Core.Messages;
+using Microsoft.AspNetCore.WebUtilities;
namespace IdentityShroud.Core.Tests;
@@ -34,6 +35,7 @@ public class UnitTest1
// Option 3: Generate a new key for testing
rsa.KeySize = 2048;
+
// Your already encoded header and payload
string header = "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJybVZ3TU5rM0o1WHlmMWhyS3NVbEVYN1BNUm42dlZKY0h3U3FYMUVQRnFJIn0";
string payload = "eyJleHAiOjE3Njk5MzY5MDksImlhdCI6MTc2OTkzNjYwOSwianRpIjoiMjNiZDJmNjktODdhYi00YmM2LWE0MWQtZGZkNzkxNDc4ZDM0IiwiaXNzIjoiaHR0cHM6Ly9pYW0ua2Fzc2FjbG91ZC5ubC9hdXRoL3JlYWxtcy9tcGx1c2thc3NhIiwiYXVkIjpbImthc3NhLW1hbmFnZW1lbnQtc2VydmljZSIsImFwYWNoZTItaW50cmFuZXQtYXV0aCIsImFjY291bnQiXSwic3ViIjoiMDkzY2NmMTUtYzRhOS00YWI0LTk3MWYtZDVhMDIyMzZkODVhIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoibXBvYmFja2VuZCIsInNpZCI6IjI2NmUyNjJiLTU5NjMtNDUyZi04ZTI3LWIwZTkzMjBkNTZkNiIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJkZWZhdWx0LXJvbGVzLW1wbHVza2Fzc2EiLCJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIiwiZGVhbGVyLW1lZGV3ZXJrZXItcm9sZSIsIm1wbHVza2Fzc2EtbWVkZXdlcmtlci1yb2xlIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYXBhY2hlMi1pbnRyYW5ldC1hdXRoIjp7InJvbGVzIjpbImludHJhbmV0IiwicmVsZWFzZW5vdGVzX3dyaXRlIl19LCJrYXNzYS1tYW5hZ2VtZW50LXNlcnZpY2UiOnsicm9sZXMiOlsicG9zYWNjb3VudF9wYXNzd29yZHJlc2V0IiwiZHJhZnRfbGljZW5zZV93cml0ZSIsImxpY2Vuc2VfcmVhZCIsImtub3dsZWRnZUl0ZW1fcmVhZCIsIm1haWxpbmdfcmVhZCIsIm1wbHVzYXBpX3JlYWQiLCJkYXRhYmFzZV91c2VyX3dyaXRlIiwiZW52aXJvbm1lbnRfd3JpdGUiLCJna3NfYXV0aGNvZGVfcmVhZCIsImVtcGxveWVlX3JlYWQiLCJkYXRhYmFzZV91c2VyX3JlYWQiLCJhcGlhY2NvdW50X3Bhc3N3b3JkcmVzZXQiLCJtcGx1c2FwaV93cml0ZSIsImVudmlyb25tZW50X3JlYWQiLCJrbm93bGVkZ2VJdGVtX3dyaXRlIiwiZGF0YWJhc2VfdXNlcl9wYXNzd29yZF9yZWFkIiwibGljZW5zZV93cml0ZSIsImN1c3RvbWVyX3dyaXRlIiwiZGVhbGVyX3JlYWQiLCJlbXBsb3llZV93cml0ZSIsImRhdGFiYXNlX2NvbmZpZ3VyYXRpb25fd3JpdGUiLCJyZWxhdGlvbnNfcmVhZCIsImRhdGFiYXNlX3VzZXJfcGFzc3dvcmRfbXBsdXNfZW5jcnlwdGVkX3JlYWQiLCJkcmFmdF9saWNlbnNlX3JlYWQiLCJkYXRhYmFzZV9jb25maWd1cmF0aW9uX3JlYWQiXX0sImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoia21zIGVtYWlsIHByb2ZpbGUiLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiZGVhbGVySWQiOjEsIm5hbWUiOiJFZWxrZSBLbGVpbiIsInByZWZlcnJlZF91c2VybmFtZSI6ImVlbGtlQGJvbHQubmwiLCJsb2NhbGUiOiJlbiIsImdpdmVuX25hbWUiOiJFZWxrZSIsImZhbWlseV9uYW1lIjoiS2xlaW4iLCJlbWFpbCI6ImVlbGtlQGJvbHQubmwiLCJlbXBsb3llZU51bWJlciI6NTR9";
@@ -49,15 +51,6 @@ public class UnitTest1
// Or generate complete JWT
// string completeJwt = JwtSignatureGenerator.GenerateCompleteJwt(header, payload, rsa);
// Console.WriteLine($"Complete JWT: {completeJwt}");
-
- rsa.ExportRSAPublicKey(); // PKCS#1
- }
-
- using (ECDsa dsa = ECDsa.Create())
- {
- dsa.ExportPkcs8PrivateKey();
-
- dsa.ExportSubjectPublicKeyInfo(); // x509
}
}
}
@@ -73,10 +66,10 @@ public static class JwtReader
return new JsonWebToken()
{
Header = JsonSerializer.Deserialize(
- Base64Url.DecodeFromChars(jwt.AsSpan().Slice(0, firstDot)))!,
+ Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(jwt, 0, firstDot))),
Payload = JsonSerializer.Deserialize(
- Base64Url.DecodeFromChars(jwt.AsSpan().Slice(firstDot + 1, secondDot - (firstDot + 1))))!,
- Signature = Base64Url.DecodeFromChars(jwt.AsSpan().Slice(secondDot + 1, jwt.Length - (secondDot + 1))),
+ Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(jwt, firstDot + 1, secondDot - (firstDot + 1)))),
+ Signature = WebEncoders.Base64UrlDecode(jwt, secondDot + 1, jwt.Length - (secondDot + 1))
};
}
}
@@ -101,5 +94,14 @@ public static class RsaKeyLoader
string pemContent = System.IO.File.ReadAllText(filePath);
return LoadFromPem(pemContent);
}
-
+
+ ///
+ /// Load RSA private key from PKCS#8 format
+ ///
+ public static RSA LoadFromPkcs8(byte[] pkcs8Key)
+ {
+ var rsa = RSA.Create();
+ rsa.ImportPkcs8PrivateKey(pkcs8Key, out _);
+ return rsa;
+ }
}
\ No newline at end of file
diff --git a/IdentityShroud.Core/Contracts/IClientService.cs b/IdentityShroud.Core/Contracts/IClientService.cs
deleted file mode 100644
index 20e270c..0000000
--- a/IdentityShroud.Core/Contracts/IClientService.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-using IdentityShroud.Core.Model;
-
-namespace IdentityShroud.Core.Contracts;
-
-public interface IClientService
-{
- Task> Create(
- Guid realmId,
- ClientCreateRequest request,
- CancellationToken ct = default);
-
- Task GetByClientId(Guid realmId, string clientId, CancellationToken ct = default);
- Task FindById(Guid realmId, int id, CancellationToken ct = default);
-}
\ No newline at end of file
diff --git a/IdentityShroud.Core/Contracts/IClock.cs b/IdentityShroud.Core/Contracts/IClock.cs
deleted file mode 100644
index 4ba7766..0000000
--- a/IdentityShroud.Core/Contracts/IClock.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-namespace IdentityShroud.Core.Contracts;
-
-public interface IClock
-{
- DateTime UtcNow();
-}
\ No newline at end of file
diff --git a/IdentityShroud.Core/Contracts/IDataEncryptionService.cs b/IdentityShroud.Core/Contracts/IDataEncryptionService.cs
deleted file mode 100644
index 1a89862..0000000
--- a/IdentityShroud.Core/Contracts/IDataEncryptionService.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-using System.Text;
-using IdentityShroud.Core.Model;
-using IdentityShroud.Core.Security;
-
-namespace IdentityShroud.Core.Contracts;
-
-public interface IDataEncryptionService
-{
- EncryptedValue Encrypt(RealmDek dek, ReadOnlySpan plain);
- byte[] Decrypt(IReadOnlyList deks, EncryptedValue input);
-}
-
-public static class DataEncryptionServiceExtensions
-{
- public static string DecryptUtf8ToString(
- this IDataEncryptionService des,
- IReadOnlyList deks,
- EncryptedValue input)
- {
- return Encoding.UTF8.GetString(des.Decrypt(deks, input));
- }
-}
\ No newline at end of file
diff --git a/IdentityShroud.Core/Contracts/IDekEncryptionService.cs b/IdentityShroud.Core/Contracts/IDekEncryptionService.cs
deleted file mode 100644
index bbb234c..0000000
--- a/IdentityShroud.Core/Contracts/IDekEncryptionService.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-using IdentityShroud.Core.Security;
-
-namespace IdentityShroud.Core.Contracts;
-
-
-
-public interface IDekEncryptionService
-{
- EncryptedDek Encrypt(ReadOnlySpan plain);
-
- void Decrypt(EncryptedDek input, Span output);
- int GetDecryptedSize(EncryptedDek input);
-}
\ No newline at end of file
diff --git a/IdentityShroud.Core/Contracts/IEncryptionService.cs b/IdentityShroud.Core/Contracts/IEncryptionService.cs
new file mode 100644
index 0000000..f85487d
--- /dev/null
+++ b/IdentityShroud.Core/Contracts/IEncryptionService.cs
@@ -0,0 +1,7 @@
+namespace IdentityShroud.Core.Contracts;
+
+public interface IEncryptionService
+{
+ byte[] Encrypt(byte[] plain);
+ byte[] Decrypt(byte[] cipher);
+}
\ No newline at end of file
diff --git a/IdentityShroud.Core/Contracts/IKeyService.cs b/IdentityShroud.Core/Contracts/IKeyService.cs
deleted file mode 100644
index 08a5bf6..0000000
--- a/IdentityShroud.Core/Contracts/IKeyService.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-using IdentityShroud.Core.Security.Keys;
-
-namespace IdentityShroud.Core.Contracts;
-
-public record CreateKeyResponse(KeyType KeyType, KeyData Key);
-
-public interface IKeyService
-{
- CreateKeyResponse CreateKey(KeyPolicy policy);
-}
\ No newline at end of file
diff --git a/IdentityShroud.Core/Contracts/IRealmContext.cs b/IdentityShroud.Core/Contracts/IRealmContext.cs
deleted file mode 100644
index c757a02..0000000
--- a/IdentityShroud.Core/Contracts/IRealmContext.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-using IdentityShroud.Core.Model;
-
-namespace IdentityShroud.Core.Contracts;
-
-public interface IRealmContext
-{
- public Realm GetRealm();
- Task> GetDeks(CancellationToken ct = default);
-}
\ No newline at end of file
diff --git a/IdentityShroud.Core/Contracts/IRealmService.cs b/IdentityShroud.Core/Contracts/IRealmService.cs
deleted file mode 100644
index 1724e6c..0000000
--- a/IdentityShroud.Core/Contracts/IRealmService.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-using IdentityShroud.Core.Messages.Realm;
-using IdentityShroud.Core.Model;
-
-namespace IdentityShroud.Core.Contracts;
-
-public interface IRealmService
-{
- Task FindById(Guid id, CancellationToken ct = default);
- Task FindBySlug(string slug, CancellationToken ct = default);
-
- Task> Create(RealmCreateRequest request, CancellationToken ct = default);
- Task LoadActiveKeys(Realm realm);
- Task LoadDeks(Realm realm);
-}
\ No newline at end of file
diff --git a/IdentityShroud.Core/Contracts/ISecretProvider.cs b/IdentityShroud.Core/Contracts/ISecretProvider.cs
index 4d4182e..73cd3a6 100644
--- a/IdentityShroud.Core/Contracts/ISecretProvider.cs
+++ b/IdentityShroud.Core/Contracts/ISecretProvider.cs
@@ -1,14 +1,6 @@
-using IdentityShroud.Core.Security;
-
namespace IdentityShroud.Core.Contracts;
public interface ISecretProvider
{
- string GetSecret(string name);
-
- ///
- /// Should return one active key, might return inactive keys.
- ///
- ///
- KeyEncryptionKey[] GetKeys(string name);
+ string GetSecretAsync(string name);
}
diff --git a/IdentityShroud.Core/CoreServiceCollectionExtensions.cs b/IdentityShroud.Core/CoreServiceCollectionExtensions.cs
deleted file mode 100644
index 86d7339..0000000
--- a/IdentityShroud.Core/CoreServiceCollectionExtensions.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-using IdentityShroud.Core.Contracts;
-using IdentityShroud.Core.EFCore;
-using IdentityShroud.Core.Security;
-using IdentityShroud.Core.Security.Keys;
-using IdentityShroud.Core.Services;
-using Microsoft.Extensions.DependencyInjection;
-
-namespace IdentityShroud.Core;
-
-public static class CoreServiceCollectionExtensions
-{
- public static IServiceCollection AddCore(this IServiceCollection services)
- {
- services.AddScoped();
-
- services.Scan(scan => scan
- .FromAssemblyOf()
- .AddClasses(classes => classes.AssignableTo())
- .AsImplementedInterfaces()
- .WithSingletonLifetime());
- services.AddSingleton();
-
- services.AddSingleton();
- services.AddSingleton();
- services.AddScoped();
- services.AddScoped();
- services.AddScoped();
- services.AddScoped();
- services.AddSingleton();
-
-
- services.AddScoped();
- services.AddScoped();
-
-
- return services;
- }
-}
\ No newline at end of file
diff --git a/IdentityShroud.Core/DTO/Client/ClientCreateRequest.cs b/IdentityShroud.Core/DTO/Client/ClientCreateRequest.cs
deleted file mode 100644
index f1c3b40..0000000
--- a/IdentityShroud.Core/DTO/Client/ClientCreateRequest.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-namespace IdentityShroud.Core.Contracts;
-
-public record ClientCreateRequest(
- string ClientId,
- string? Name = null,
- string? Description = null,
- string? SignatureAlgorithm = null,
- bool Confidential = false,
- bool AllowClientCredentialsFlow = false,
- bool GenerateSecret = false);
\ No newline at end of file
diff --git a/IdentityShroud.Core/DTO/JsonWebKey.cs b/IdentityShroud.Core/DTO/JsonWebKey.cs
index afc9367..e22a899 100644
--- a/IdentityShroud.Core/DTO/JsonWebKey.cs
+++ b/IdentityShroud.Core/DTO/JsonWebKey.cs
@@ -1,50 +1,34 @@
using System.Text.Json.Serialization;
-using IdentityShroud.Core.Helpers;
-using IdentityShroud.Core.Security.Keys;
namespace IdentityShroud.Core.Messages;
-// https://www.rfc-editor.org/rfc/rfc7517.html
-
-
public class JsonWebKey
{
[JsonPropertyName("kty")]
- public required KeyType KeyType { get; set; }
+ public string KeyType { get; set; } = "RSA";
- // Common values sig(nature) enc(ryption)
[JsonPropertyName("use")]
- public string? Use { get; set; } = "sig"; // "sig" for signature, "enc" for encryption
+ public string Use { get; set; } = "sig"; // "sig" for signature, "enc" for encryption
- // Per standard this field is optional, commented out for now as it seems not
- // have any good use in an identity server. Anyone validating tokens should use
- // the algorithm specified in the header of the token.
- // [JsonPropertyName("alg")]
- // public string? Algorithm { get; set; } = "RS256";
+ [JsonPropertyName("alg")]
+ public string Algorithm { get; set; } = "RS256";
[JsonPropertyName("kid")]
- public required string KeyId { get; set; }
+ public string KeyId { get; set; }
// RSA Public Key Components
[JsonPropertyName("n")]
- public string? Modulus { get; set; }
+ public string Modulus { get; set; }
[JsonPropertyName("e")]
- public string? Exponent { get; set; }
-
- // ECdsa
- public string? Curve { get; set; }
- [JsonConverter(typeof(Base64UrlConverter))]
- public byte[]? X { get; set; }
- [JsonConverter(typeof(Base64UrlConverter))]
- public byte[]? Y { get; set; }
+ public string Exponent { get; set; }
// Optional fields
- // [JsonPropertyName("x5c")]
- // [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
- // public List? X509CertificateChain { get; set; }
- //
- // [JsonPropertyName("x5t")]
- // [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
- // public string? X509CertificateThumbprint { get; set; }
+ [JsonPropertyName("x5c")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public List X509CertificateChain { get; set; }
+
+ [JsonPropertyName("x5t")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string X509CertificateThumbprint { get; set; }
}
\ No newline at end of file
diff --git a/IdentityShroud.Core/DTO/JsonWebToken.cs b/IdentityShroud.Core/DTO/JsonWebToken.cs
index 75b7dae..65d671f 100644
--- a/IdentityShroud.Core/DTO/JsonWebToken.cs
+++ b/IdentityShroud.Core/DTO/JsonWebToken.cs
@@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
-namespace IdentityShroud.Core.DTO;
+namespace IdentityShroud.Core.Messages;
public class JsonWebTokenHeader
{
@@ -9,32 +9,31 @@ public class JsonWebTokenHeader
[JsonPropertyName("typ")]
public string Type { get; set; } = "JWT";
[JsonPropertyName("kid")]
- public required string KeyId { get; set; }
+ public string KeyId { get; set; }
}
-//
public class JsonWebTokenPayload
{
[JsonPropertyName("iss")]
- public string? Issuer { get; set; }
+ public string Issuer { get; set; }
[JsonPropertyName("aud")]
- public string[]? Audience { get; set; }
+ public string[] Audience { get; set; }
[JsonPropertyName("sub")]
- public string? Subject { get; set; }
+ public string Subject { get; set; }
[JsonPropertyName("exp")]
- public long? Expires { get; set; }
+ public long Expires { get; set; }
[JsonPropertyName("iat")]
- public long? IssuedAt { get; set; }
+ public long IssuedAt { get; set; }
[JsonPropertyName("nbf")]
- public long? NotBefore { get; set; }
+ public long NotBefore { get; set; }
[JsonPropertyName("jti")]
- public Guid? JwtId { get; set; }
+ public Guid JwtId { get; set; }
}
public class JsonWebToken
{
- public required JsonWebTokenHeader Header { get; set; }
- public required JsonWebTokenPayload Payload { get; set; }
- public required byte[] Signature { get; set; } = [];
+ public JsonWebTokenHeader Header { get; set; } = new();
+ public JsonWebTokenPayload Payload { get; set; } = new();
+ public byte[] Signature { get; set; } = [];
}
\ No newline at end of file
diff --git a/IdentityShroud.Core/DTO/OpenId/GrantTypes.cs b/IdentityShroud.Core/DTO/OpenId/GrantTypes.cs
deleted file mode 100644
index e764e24..0000000
--- a/IdentityShroud.Core/DTO/OpenId/GrantTypes.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-using System.Text.Json.Serialization;
-
-namespace IdentityShroud.Core.DTO.OpenId;
-
-public enum GrantTypes
-{
- [JsonStringEnumMemberName("client_credentials")]
- ClientCredentials
-}
\ No newline at end of file
diff --git a/IdentityShroud.Core/DTO/OpenId/TokenResponse.cs b/IdentityShroud.Core/DTO/OpenId/TokenResponse.cs
deleted file mode 100644
index 23d9718..0000000
--- a/IdentityShroud.Core/DTO/OpenId/TokenResponse.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using System.Text.Json.Serialization;
-
-namespace IdentityShroud.Core.Services.OpenId;
-
-public class TokenResponse
-{
- [JsonPropertyName("access_token")]
- public required string AccessToken { get; set; }
-
- [JsonPropertyName("token_type")]
- public required string TokenType { get; set; }
-
- [JsonPropertyName("expires_in")]
- public int? ExpiresIn { get; set; }
-
- [JsonPropertyName("refresh_token")]
- public string? RefreshToken { get; set; }
-
-}
\ No newline at end of file
diff --git a/IdentityShroud.Core/DTO/Realm/RealmCreateRequest.cs b/IdentityShroud.Core/DTO/Realm/RealmCreateRequest.cs
index 143c75b..fab91aa 100644
--- a/IdentityShroud.Core/DTO/Realm/RealmCreateRequest.cs
+++ b/IdentityShroud.Core/DTO/Realm/RealmCreateRequest.cs
@@ -1,3 +1,3 @@
namespace IdentityShroud.Core.Messages.Realm;
-public record RealmCreateRequest(Guid? Id = null, string? Slug = null, string? Name = null);
\ No newline at end of file
+public record RealmCreateRequest(Guid? Id, string? Slug, string Name);
\ No newline at end of file
diff --git a/IdentityShroud.Core/EFCore/Db.cs b/IdentityShroud.Core/Db.cs
similarity index 52%
rename from IdentityShroud.Core/EFCore/Db.cs
rename to IdentityShroud.Core/Db.cs
index b2bc12e..2f95902 100644
--- a/IdentityShroud.Core/EFCore/Db.cs
+++ b/IdentityShroud.Core/Db.cs
@@ -1,11 +1,9 @@
using IdentityShroud.Core.Model;
-using IdentityShroud.Core.Security;
-using IdentityShroud.Core.Security.Keys;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
-namespace IdentityShroud.Core.EFCore;
+namespace IdentityShroud.Core;
public class DbConfiguration
{
@@ -18,11 +16,8 @@ public class Db(
ILoggerFactory? loggerFactory)
: DbContext
{
- public virtual DbSet Clients { get; set; }
public virtual DbSet Realms { get; set; }
- public virtual DbSet Keys { get; set; }
- public virtual DbSet Deks { get; set; }
-
+
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseNpgsql("");
@@ -38,22 +33,6 @@ public class Db(
{
optionsBuilder.UseLoggerFactory(loggerFactory);
}
- }
-
- protected override void OnModelCreating(ModelBuilder modelBuilder)
- {
- modelBuilder.ApplyConfigurationsFromAssembly(typeof(Db).Assembly);
- }
-
- protected override void ConfigureConventions(ModelConfigurationBuilder b)
- {
- base.ConfigureConventions(b);
- b.Properties().HaveConversion();
- b.Properties>().HaveConversion>();
- b.Properties().HaveConversion();
- b.Properties().HaveConversion();
- b.Properties().HaveConversion();
- b.Properties().HaveConversion();
}
}
\ No newline at end of file
diff --git a/IdentityShroud.Core/EFCore/Converters/DekIdConverter.cs b/IdentityShroud.Core/EFCore/Converters/DekIdConverter.cs
deleted file mode 100644
index df12fc2..0000000
--- a/IdentityShroud.Core/EFCore/Converters/DekIdConverter.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-using IdentityShroud.Core.Security;
-using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
-
-namespace IdentityShroud.Core.EFCore;
-
-public class DekIdConverter() : ValueConverter(id => id.Id, guid => new DekId(guid));
\ No newline at end of file
diff --git a/IdentityShroud.Core/EFCore/Converters/DictionaryToJsonConverter.cs b/IdentityShroud.Core/EFCore/Converters/DictionaryToJsonConverter.cs
deleted file mode 100644
index 1236b67..0000000
--- a/IdentityShroud.Core/EFCore/Converters/DictionaryToJsonConverter.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-using System.Text.Json;
-using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
-
-namespace IdentityShroud.Core.EFCore;
-
-public class DictionaryToJsonConverter : ValueConverter, string>
- where TKey : notnull
-{
- public DictionaryToJsonConverter() : base(
- v => JsonSerializer.Serialize(v),
- v => JsonSerializer.Deserialize>(v) ?? new())
- {
- }
-}
\ No newline at end of file
diff --git a/IdentityShroud.Core/EFCore/Converters/JwtSigAlgNameConverter.cs b/IdentityShroud.Core/EFCore/Converters/JwtSigAlgNameConverter.cs
deleted file mode 100644
index d570d61..0000000
--- a/IdentityShroud.Core/EFCore/Converters/JwtSigAlgNameConverter.cs
+++ /dev/null
@@ -1,5 +0,0 @@
-using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
-
-namespace IdentityShroud.Core.EFCore;
-
-public class JwtSigAlgNameConverter() : ValueConverter(j => j.ToString(), s => new(s));
\ No newline at end of file
diff --git a/IdentityShroud.Core/EFCore/Converters/KekIdConverter.cs b/IdentityShroud.Core/EFCore/Converters/KekIdConverter.cs
deleted file mode 100644
index 23f55fe..0000000
--- a/IdentityShroud.Core/EFCore/Converters/KekIdConverter.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using IdentityShroud.Core.Security;
-using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
-
-namespace IdentityShroud.Core.EFCore;
-
-public class KekIdConverter : ValueConverter
-{
- public KekIdConverter()
- : base(id => id.Id, guid => new KekId(guid))
- {
- }
-}
\ No newline at end of file
diff --git a/IdentityShroud.Core/EFCore/Converters/KeyTypeConverter.cs b/IdentityShroud.Core/EFCore/Converters/KeyTypeConverter.cs
deleted file mode 100644
index 18c8574..0000000
--- a/IdentityShroud.Core/EFCore/Converters/KeyTypeConverter.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-using IdentityShroud.Core.Security.Keys;
-using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
-
-namespace IdentityShroud.Core.EFCore;
-
-public class KeyTypeConverter() : ValueConverter(id => id.ToString(), s => new(s));
\ No newline at end of file
diff --git a/IdentityShroud.Core/EFCore/Converters/RealmSigningKeyIdConverter.cs b/IdentityShroud.Core/EFCore/Converters/RealmSigningKeyIdConverter.cs
deleted file mode 100644
index f36ff9a..0000000
--- a/IdentityShroud.Core/EFCore/Converters/RealmSigningKeyIdConverter.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-using IdentityShroud.Core.Model;
-using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
-
-namespace IdentityShroud.Core.EFCore;
-
-public class RealmSigningKeyIdConverter : ValueConverter
-{
- public RealmSigningKeyIdConverter()
- : base(id => id.Id, guid => new RealmSigningKeyId(guid))
- {
- }
-
-}
\ No newline at end of file
diff --git a/IdentityShroud.Core/Helpers/Base64UrlConverter.cs b/IdentityShroud.Core/Helpers/Base64UrlConverter.cs
deleted file mode 100644
index 77f05f2..0000000
--- a/IdentityShroud.Core/Helpers/Base64UrlConverter.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-using System.Buffers;
-using System.Buffers.Text;
-using System.Text.Json;
-using System.Text.Json.Serialization;
-
-namespace IdentityShroud.Core.Helpers;
-
-public class Base64UrlConverter : JsonConverter
-{
- public override byte[] Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
- {
- // GetValueSpan gives you the raw UTF-8 bytes of the JSON string value
- if (reader.HasValueSequence)
- {
- var valueSequence = reader.ValueSequence.ToArray();
- return Base64Url.DecodeFromUtf8(valueSequence);
- }
- return Base64Url.DecodeFromUtf8(reader.ValueSpan);
- }
-
- public override void Write(Utf8JsonWriter writer, byte[] value, JsonSerializerOptions options)
- {
- int encodedLength = Base64Url.GetEncodedLength(value.Length);
- Span buffer = encodedLength <= 256 ? stackalloc byte[encodedLength] : new byte[encodedLength];
- Base64Url.EncodeToUtf8(value, buffer);
- writer.WriteStringValue(buffer);
- }
-}
\ No newline at end of file
diff --git a/IdentityShroud.Core/Helpers/SlugHelper.cs b/IdentityShroud.Core/Helpers/SlugHelper.cs
index 51aa0c3..0c74455 100644
--- a/IdentityShroud.Core/Helpers/SlugHelper.cs
+++ b/IdentityShroud.Core/Helpers/SlugHelper.cs
@@ -1,3 +1,4 @@
+using System;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
@@ -72,9 +73,9 @@ public static class SlugHelper
private static string GenerateHashSuffix(string text)
{
- using (var md5 = MD5.Create())
+ using (var sha256 = SHA256.Create())
{
- byte[] hash = md5.ComputeHash(Encoding.UTF8.GetBytes(text));
+ byte[] hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(text));
// Take first 4 bytes (will become ~5-6 base64url chars)
string base64Url = WebEncoders.Base64UrlEncode(hash, 0, 4);
diff --git a/IdentityShroud.Core/IdentityShroud.Core.csproj b/IdentityShroud.Core/IdentityShroud.Core.csproj
index fe5ed22..a87c996 100644
--- a/IdentityShroud.Core/IdentityShroud.Core.csproj
+++ b/IdentityShroud.Core/IdentityShroud.Core.csproj
@@ -1,4 +1,4 @@
-
+
net10.0
@@ -7,24 +7,22 @@
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
+
-
+
+ ..\..\..\.nuget\packages\microsoft.aspnetcore.webutilities\10.0.2\lib\net10.0\Microsoft.AspNetCore.WebUtilities.dll
+
diff --git a/IdentityShroud.Core/IdentityShroud.Core.csproj.DotSettings b/IdentityShroud.Core/IdentityShroud.Core.csproj.DotSettings
deleted file mode 100644
index f42aea1..0000000
--- a/IdentityShroud.Core/IdentityShroud.Core.csproj.DotSettings
+++ /dev/null
@@ -1,2 +0,0 @@
-
- True
\ No newline at end of file
diff --git a/IdentityShroud.Core/Model/Client.cs b/IdentityShroud.Core/Model/Client.cs
index b7d9c60..0be04ed 100644
--- a/IdentityShroud.Core/Model/Client.cs
+++ b/IdentityShroud.Core/Model/Client.cs
@@ -1,37 +1,7 @@
-using System.ComponentModel.DataAnnotations;
-using System.ComponentModel.DataAnnotations.Schema;
-using Microsoft.EntityFrameworkCore;
-
namespace IdentityShroud.Core.Model;
-[Table("client")]
-[Index(nameof(ClientId), IsUnique = true)]
public class Client
{
- [Key]
- public int Id { 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; }
-
- [MaxLength(20)]
- public JwtSigAlgName? SignatureAlgorithm { get; set; }
-
- ///
- /// Enables confidential flows
- ///
- public bool Confidential { get; set; }
-
- ///
- /// Enables the client credentials flow which required Confidential to be true too.
- ///
- public bool AllowClientCredentialsFlow { get; set; } = false;
-
- public required DateTime CreatedAt { get; set; }
-
- public List Secrets { get; set; } = [];
+ public Guid Id { get; set; }
+ public string Name { get; set; }
}
\ No newline at end of file
diff --git a/IdentityShroud.Core/Model/ClientSecret.cs b/IdentityShroud.Core/Model/ClientSecret.cs
deleted file mode 100644
index 189039f..0000000
--- a/IdentityShroud.Core/Model/ClientSecret.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-using System.ComponentModel.DataAnnotations;
-using System.ComponentModel.DataAnnotations.Schema;
-using IdentityShroud.Core.Security;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Metadata.Builders;
-
-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? Expires { get; set; }
- public DateTime? RevokedAt { get; set; }
- public required EncryptedValue Secret { get; set; }
-}
-
-public class ClientSecretConfiguration : IEntityTypeConfiguration
-{
- public void Configure(EntityTypeBuilder b)
- {
- b.ToTable("client_secret");
- b.HasKey(e => e.Id);
- b.ComplexProperty(e => e.Secret);
- }
-}
\ No newline at end of file
diff --git a/IdentityShroud.Core/Model/DecryptedSigningKey.cs b/IdentityShroud.Core/Model/DecryptedSigningKey.cs
deleted file mode 100644
index 4a94dc7..0000000
--- a/IdentityShroud.Core/Model/DecryptedSigningKey.cs
+++ /dev/null
@@ -1,66 +0,0 @@
-using System.Security.Cryptography;
-using IdentityShroud.Core.Contracts;
-using IdentityShroud.Core.Security.Keys;
-
-namespace IdentityShroud.Core.Model;
-
-public sealed class DecryptedSigningKey : IDisposable
-{
- private readonly byte[] _keyData;
- private readonly int _keyLength;
- private bool _disposed;
-
- public RealmSigningKeyId Id { get; }
- public KeyType KeyType { get; }
- public ReadOnlySpan KeyData => _disposed
- ? throw new ObjectDisposedException(nameof(DecryptedSigningKey))
- : _keyData.AsSpan(0, _keyLength);
-
- public DecryptedSigningKey(RealmSigningKey realmSigningKey, IDekEncryptionService encryptionService)
- {
- Id = realmSigningKey.Id;
- KeyType = realmSigningKey.KeyType;
- int keySize = encryptionService.GetDecryptedSize(realmSigningKey.Key);
- _keyData = GC.AllocateArray(keySize, pinned: true);
- _keyLength = keySize;
- encryptionService.Decrypt(realmSigningKey.Key, _keyData);
- }
-
- public DecryptedSigningKey()
- {
- Id = RealmSigningKeyId.NewId();
- KeyType = KeyType.RSA;
- const int keySize = 2048;
-
- using var rsa = RSA.Create();
- rsa.KeySize = keySize;
- int estimatedSize = EstimatePkcs8ExportSize(keySize);
-
- Span temp = stackalloc byte[estimatedSize * 2];
- try
- {
- if (!rsa.TryExportPkcs8PrivateKey(temp, out int bytesWritten))
- throw new CryptographicException("Unable to export RSA private key.");
-
- _keyData = GC.AllocateArray(bytesWritten, pinned: true);
- _keyLength = bytesWritten;
- temp[..bytesWritten].CopyTo(_keyData);
- }
- finally
- {
- CryptographicOperations.ZeroMemory(temp);
- }
- }
-
-
- public void Dispose()
- {
- if (_disposed) return;
- _disposed = true;
- CryptographicOperations.ZeroMemory(_keyData);
- }
-
- // Note actual accurate coefficients would be *0.566 and +57.4
- public static int EstimatePkcs8ExportSize(int keySizeBits)
- => ((keySizeBits * 6) / 10) + 150;
-}
\ No newline at end of file
diff --git a/IdentityShroud.Core/Model/Realm.cs b/IdentityShroud.Core/Model/Realm.cs
index 97f08c7..5fc9639 100644
--- a/IdentityShroud.Core/Model/Realm.cs
+++ b/IdentityShroud.Core/Model/Realm.cs
@@ -1,11 +1,14 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
+using IdentityShroud.Core.Contracts;
namespace IdentityShroud.Core.Model;
[Table("realm")]
public class Realm
{
+ private byte[] _privateKeyDecrypted = [];
+
public Guid Id { get; set; }
///
/// Note this is part of the url we should encourage users to keep it short but we do not want to limit them too much
@@ -17,17 +20,26 @@ public class Realm
public string Name { get; set; } = "";
public List Clients { get; init; } = [];
-
- ///
- /// Note multiple keys can be in use at the same time because different clients may be configured to use
- /// a different keytype depending on their clients requirements/capabilities.
- ///
- public List TokenSigningKeys { get; init; } = [];
+ public byte[] PrivateKeyEncrypted
+ {
+ get;
+ set
+ {
+ field = value;
+ _privateKeyDecrypted = [];
+ }
+ } = [];
- public List DataEncryptionKeys { get; init; } = [];
+ public byte[] GetPrivateKey(IEncryptionService encryptionService)
+ {
+ if (_privateKeyDecrypted.Length == 0 && PrivateKeyEncrypted.Length > 0)
+ _privateKeyDecrypted = encryptionService.Decrypt(PrivateKeyEncrypted);
+ return _privateKeyDecrypted;
+ }
- ///
- /// Can be overriden per client
- ///
- public JwtSigAlgName DefaultSignatureAlgorithm { get; set; } = JwtSigAlgName.RS256;
+ public void SetPrivateKey(IEncryptionService encryptionService, byte[] privateKey)
+ {
+ PrivateKeyEncrypted = encryptionService.Encrypt(privateKey);
+ _privateKeyDecrypted = privateKey;
+ }
}
\ No newline at end of file
diff --git a/IdentityShroud.Core/Model/RealmDek.cs b/IdentityShroud.Core/Model/RealmDek.cs
deleted file mode 100644
index 92bc57b..0000000
--- a/IdentityShroud.Core/Model/RealmDek.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-using IdentityShroud.Core.Security;
-using IdentityShroud.Core.Security.Keys;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Metadata.Builders;
-
-namespace IdentityShroud.Core.Model;
-
-
-public record RealmDek
-{
- public required DekId Id { get; init; }
- public required bool Active { get; set; }
- public required KeyType Algorithm { get; init; }
- public required EncryptedDek KeyData { get; init; }
- public Guid RealmId { get; init; }
-}
-
-public class RealmDekConfiguration : IEntityTypeConfiguration
-{
- public void Configure(EntityTypeBuilder b)
- {
- b.ToTable("realm_dek");
- b.HasKey(e => e.Id);
- b.ComplexProperty(e => e.KeyData, e => e.IsRequired());
- }
-}
-
diff --git a/IdentityShroud.Core/Model/RealmSigningKey.cs b/IdentityShroud.Core/Model/RealmSigningKey.cs
deleted file mode 100644
index 25b37a2..0000000
--- a/IdentityShroud.Core/Model/RealmSigningKey.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-using IdentityShroud.Core.Security;
-using IdentityShroud.Core.Security.Keys;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Metadata.Builders;
-
-namespace IdentityShroud.Core.Model;
-
-public record RealmSigningKey
-{
- public required RealmSigningKeyId Id { get; init; }
- public required KeyType KeyType { get; init; }
- public required EncryptedDek Key { get; init; }
- public required DateTime CreatedAt { get; init; }
- public DateTime? RevokedAt { get; set; }
- ///
- /// 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.
- ///
- public int Priority { get; set; } = 10;
-
- public Dictionary? PublicKeyParameters { get; set; }
-}
-
-public class RealmKeyConfiguration : IEntityTypeConfiguration
-{
- public void Configure(EntityTypeBuilder b)
- {
- b.ToTable("realm_key");
- b.HasKey(e => e.Id);
-
- b.ComplexProperty(e => e.Key, e => e.IsRequired());
- b.Property(e => e.PublicKeyParameters).HasColumnType("jsonb");
- }
-}
diff --git a/IdentityShroud.Core/Model/RealmSigningKeyId.cs b/IdentityShroud.Core/Model/RealmSigningKeyId.cs
deleted file mode 100644
index 085b9ff..0000000
--- a/IdentityShroud.Core/Model/RealmSigningKeyId.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-using System.Text.Json;
-using System.Text.Json.Serialization;
-
-namespace IdentityShroud.Core.Model;
-
-[JsonConverter(typeof(RealmSigningKeyIdJsonConverter))]
-public readonly record struct RealmSigningKeyId(Guid Id)
-{
- public override string ToString() => Id.ToString("N");
-
- public static RealmSigningKeyId NewId()
- {
- return new(Guid.NewGuid());
- }
-}
-
-public class RealmSigningKeyIdJsonConverter : JsonConverter
-{
- public override RealmSigningKeyId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
- => new (reader.GetGuid());
-
- public override void Write(Utf8JsonWriter writer, RealmSigningKeyId value, JsonSerializerOptions options)
- => writer.WriteStringValue(value.ToString());
-}
diff --git a/IdentityShroud.Core/Plugins/PluginLoader.cs b/IdentityShroud.Core/Plugins/PluginLoader.cs
deleted file mode 100644
index e216a57..0000000
--- a/IdentityShroud.Core/Plugins/PluginLoader.cs
+++ /dev/null
@@ -1,59 +0,0 @@
-using System.Reflection;
-using System.Runtime.Loader;
-using IdentityShroud.PluginSupport;
-
-namespace IdentityShroud.Core.Plugins;
-
-public static class PluginLoader
-{
- public static IEnumerable LoadPlugins(string pluginsFolder)
- {
- if (!Directory.Exists(pluginsFolder))
- yield break;
-
- foreach (var dll in Directory.EnumerateFiles(pluginsFolder, "*.dll"))
- {
- foreach (var plugin in LoadPluginDll(dll)) yield return plugin;
- }
- }
-
- private static IEnumerable LoadPluginDll(string dll)
- {
- Assembly asm;
- try
- {
- asm = AssemblyLoadContext.Default.LoadFromAssemblyPath(Path.GetFullPath(dll));
- }
- catch
- {
- yield break;
- }
-
- IEnumerable pluginTypes;
- try
- {
- pluginTypes = asm.GetTypes()
- .Where(t => typeof(IPlugin).IsAssignableFrom(t) && t is { IsInterface: false, IsAbstract: false });
- }
- catch
- {
- yield break;
- }
-
- foreach (var t in pluginTypes)
- {
- IPlugin? instance = null;
- try
- {
- instance = (IPlugin?)Activator.CreateInstance(t);
- }
- catch
- {
- // ignore bad plugin types
- }
-
- if (instance != null)
- yield return instance;
- }
- }
-}
\ No newline at end of file
diff --git a/IdentityShroud.Core/Plugins/PluginRegistry.cs b/IdentityShroud.Core/Plugins/PluginRegistry.cs
deleted file mode 100644
index d58863c..0000000
--- a/IdentityShroud.Core/Plugins/PluginRegistry.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using System.Collections.ObjectModel;
-using IdentityShroud.PluginSupport;
-
-namespace IdentityShroud.Core.Plugins;
-
-///
-/// Note
-///
-///
-public class PluginRegistry where TPlugin : IPlugin
-{
- private ReadOnlyDictionary _plugins;
-
- public PluginRegistry(ReadOnlyDictionary plugins)
- {
- _plugins = plugins;
- }
-}
\ No newline at end of file
diff --git a/IdentityShroud.Core/Security/AesGcmHelper.cs b/IdentityShroud.Core/Security/AesGcmHelper.cs
new file mode 100644
index 0000000..1f0e9de
--- /dev/null
+++ b/IdentityShroud.Core/Security/AesGcmHelper.cs
@@ -0,0 +1,64 @@
+using System.Security.Cryptography;
+
+namespace IdentityShroud.Core.Security;
+
+public static class AesGcmHelper
+{
+
+ public static byte[] EncryptAesGcm(byte[] plaintext, byte[] key)
+ {
+ using var aes = new AesGcm(key);
+ byte[] nonce = RandomNumberGenerator.GetBytes(AesGcm.NonceByteSizes.MaxSize);
+ byte[] ciphertext = new byte[plaintext.Length];
+ byte[] tag = new byte[AesGcm.TagByteSizes.MaxSize];
+
+ aes.Encrypt(nonce, plaintext, ciphertext, tag);
+ // Return concatenated nonce|ciphertext|tag (or store separately)
+ return nonce.Concat(ciphertext).Concat(tag).ToArray();
+ }
+
+ // --------------------------------------------------------------------
+ // DecryptAesGcm
+ // • key – 32‑byte (256‑bit) secret key (same key used for encryption)
+ // • payload – byte[] containing nonce‖ciphertext‖tag
+ // • returns – the original plaintext bytes
+ // --------------------------------------------------------------------
+ public static byte[] DecryptAesGcm(byte[] payload, byte[] key)
+ {
+ if (payload == null) throw new ArgumentNullException(nameof(payload));
+ if (key == null) throw new ArgumentNullException(nameof(key));
+ if (key.Length != 32) // 256‑bit key
+ throw new ArgumentException("Key must be 256 bits (32 bytes) for AES‑256‑GCM.", nameof(key));
+
+ // ----------------------------------------------------------------
+ // 1️⃣ Extract the three components.
+ // ----------------------------------------------------------------
+ // AesGcm.NonceByteSizes.MaxSize = 12 bytes (standard GCM nonce length)
+ // AesGcm.TagByteSizes.MaxSize = 16 bytes (128‑bit authentication tag)
+ int nonceSize = AesGcm.NonceByteSizes.MaxSize; // 12
+ int tagSize = AesGcm.TagByteSizes.MaxSize; // 16
+
+ if (payload.Length < nonceSize + tagSize)
+ throw new ArgumentException("Payload is too short to contain nonce, ciphertext, and tag.", nameof(payload));
+
+ ReadOnlySpan nonce = new(payload, 0, nonceSize);
+ ReadOnlySpan ciphertext = new(payload, nonceSize, payload.Length - nonceSize - tagSize);
+ ReadOnlySpan tag = new(payload, payload.Length - tagSize, tagSize);
+
+
+ byte[] plaintext = new byte[ciphertext.Length];
+
+ using var aes = new AesGcm(key);
+ try
+ {
+ aes.Decrypt(nonce, ciphertext, tag, plaintext);
+ }
+ catch (CryptographicException ex)
+ {
+ // Tag verification failed → tampering or wrong key/nonce.
+ throw new InvalidOperationException("Decryption failed – authentication tag mismatch.", ex);
+ }
+
+ return plaintext;
+ }
+}
\ No newline at end of file
diff --git a/IdentityShroud.Core/Security/ConfigurationSecretProvider.cs b/IdentityShroud.Core/Security/ConfigurationSecretProvider.cs
index 9355c0b..01be0a9 100644
--- a/IdentityShroud.Core/Security/ConfigurationSecretProvider.cs
+++ b/IdentityShroud.Core/Security/ConfigurationSecretProvider.cs
@@ -10,13 +10,8 @@ public class ConfigurationSecretProvider(IConfiguration configuration) : ISecret
{
private readonly IConfigurationSection secrets = configuration.GetSection("secrets");
- public string GetSecret(string name)
+ public string GetSecretAsync(string name)
{
return secrets.GetValue(name) ?? "";
}
-
- public KeyEncryptionKey[] GetKeys(string name)
- {
- return secrets.GetSection(name).Get() ?? [];
- }
}
\ No newline at end of file
diff --git a/IdentityShroud.Core/Security/DekId.cs b/IdentityShroud.Core/Security/DekId.cs
deleted file mode 100644
index d68a985..0000000
--- a/IdentityShroud.Core/Security/DekId.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace IdentityShroud.Core.Security;
-
-public readonly record struct DekId(Guid Id)
-{
- public static DekId NewId() => new(Guid.NewGuid());
-
- public override string ToString() => Id.ToString("N");
-}
\ No newline at end of file
diff --git a/IdentityShroud.Core/Security/EncryptedDek.cs b/IdentityShroud.Core/Security/EncryptedDek.cs
deleted file mode 100644
index 2e44afe..0000000
--- a/IdentityShroud.Core/Security/EncryptedDek.cs
+++ /dev/null
@@ -1,3 +0,0 @@
-namespace IdentityShroud.Core.Security;
-
-public record EncryptedDek(KekId KekId, byte[] Value);
\ No newline at end of file
diff --git a/IdentityShroud.Core/Security/EncryptedValue.cs b/IdentityShroud.Core/Security/EncryptedValue.cs
deleted file mode 100644
index 03dad86..0000000
--- a/IdentityShroud.Core/Security/EncryptedValue.cs
+++ /dev/null
@@ -1,5 +0,0 @@
-namespace IdentityShroud.Core.Security;
-
-public record EncryptedValue(DekId DekId, byte[] Value);
-
-
diff --git a/IdentityShroud.Core/Security/Encryption.cs b/IdentityShroud.Core/Security/Encryption.cs
deleted file mode 100644
index 01c8843..0000000
--- a/IdentityShroud.Core/Security/Encryption.cs
+++ /dev/null
@@ -1,79 +0,0 @@
-using System.Security.Cryptography;
-
-namespace IdentityShroud.Core.Security;
-
-public static class Encryption
-{
- private readonly record struct AlgVersion(int Version, int NonceSize, int TagSize);
-
- private static AlgVersion[] _versions =
- [
- new(0, 0, 0), // version 0 does not realy exist
- new(1, 12, 16), // version 1
- ];
-
- public static byte[] Encrypt(ReadOnlySpan plaintext, ReadOnlySpan key)
- {
- const int versionNumber = 1;
- AlgVersion versionParams = _versions[versionNumber];
-
- int resultSize = 1 + versionParams.NonceSize + versionParams.TagSize + plaintext.Length;
- // allocate buffer for complete response
- var result = new byte[resultSize];
-
- result[0] = (byte)versionParams.Version;
-
- // make the spans that point to the parts of the result where their data is located
- var nonce = result.AsSpan(1, versionParams.NonceSize);
- var tag = result.AsSpan(1 + versionParams.NonceSize, versionParams.TagSize);
- var cipher = result.AsSpan(1 + versionParams.NonceSize + versionParams.TagSize);
-
- // use the spans to place the data directly in its place
- RandomNumberGenerator.Fill(nonce);
- using var aes = new AesGcm(key, versionParams.TagSize);
- aes.Encrypt(nonce, plaintext, cipher, tag);
- return result;
- }
-
- public static void Decrypt(ReadOnlyMemory input, ReadOnlySpan key, Span output)
- {
- AlgVersion versionParams = GetVersionParams(input);
- if (input.Length < 1 + versionParams.NonceSize + versionParams.TagSize)
- throw new ArgumentException("Cypher data is too short to be valid.", nameof(input));
-
- var payload = input.Span;
- ReadOnlySpan nonce = payload.Slice(1, versionParams.NonceSize);
- ReadOnlySpan tag = payload.Slice(1 + versionParams.NonceSize, versionParams.TagSize);
- ReadOnlySpan cipher = payload.Slice(1 + versionParams.NonceSize + versionParams.TagSize);
-
- using var aes = new AesGcm(key, versionParams.TagSize);
- try
- {
- aes.Decrypt(nonce, cipher, tag, output);
- }
- catch (CryptographicException ex)
- {
- // Tag verification failed → tampering or wrong key/nonce.
- throw new InvalidOperationException("Decryption failed – authentication tag mismatch.", ex);
- }
- }
-
- public static int GetDecryptedLength(ReadOnlyMemory input)
- {
- AlgVersion versionParams = GetVersionParams(input);
- int length = input.Length - (1 + versionParams.NonceSize + versionParams.TagSize);
- if (length < 0)
- throw new ArgumentException("Cypher data is too short to be valid.", nameof(input));
-
- return length;
- }
-
- private static AlgVersion GetVersionParams(ReadOnlyMemory input)
- {
- var versionNumber = (int)input.Span[0];
- if (versionNumber != 1)
- throw new ArgumentException("Invalid payload");
-
- return _versions[versionNumber];
- }
-}
\ No newline at end of file
diff --git a/IdentityShroud.Core/Security/Jwt/IJwtSigner.cs b/IdentityShroud.Core/Security/Jwt/IJwtSigner.cs
deleted file mode 100644
index 80fc37e..0000000
--- a/IdentityShroud.Core/Security/Jwt/IJwtSigner.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-using System.Text.Json;
-using IdentityShroud.Core.Model;
-
-namespace IdentityShroud.Core;
-
-public interface IJwtSigner
-{
- /*
- Of the signature and MAC algorithms specified in JSON Web Algorithms
- [JWA], only HMAC SHA-256 ("HS256") and "none" MUST be implemented by
- conforming JWT implementations. It is RECOMMENDED that
- implementations also support RSASSA-PKCS1-v1_5 with the SHA-256 hash
- algorithm ("RS256") and ECDSA using the P-256 curve and the SHA-256
- hash algorithm ("ES256"). Support for other algorithms and key sizes
- is OPTIONAL.
- */
- IReadOnlyList Algorithms { get; }
-
- byte[] CalculateSignature(JwtSigAlgName algName, DecryptedSigningKey key, ReadOnlySpan jwt);
-}
\ No newline at end of file
diff --git a/IdentityShroud.Core/Security/Jwt/IJwtSignerFactory.cs b/IdentityShroud.Core/Security/Jwt/IJwtSignerFactory.cs
deleted file mode 100644
index fbab369..0000000
--- a/IdentityShroud.Core/Security/Jwt/IJwtSignerFactory.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-namespace IdentityShroud.Core;
-
-public interface IJwtSignerFactory
-{
- IJwtSigner Create(JwtSigAlgName algorithm);
-}
\ No newline at end of file
diff --git a/IdentityShroud.Core/Security/Jwt/JwtSigAlgName.cs b/IdentityShroud.Core/Security/Jwt/JwtSigAlgName.cs
deleted file mode 100644
index 7e59dbb..0000000
--- a/IdentityShroud.Core/Security/Jwt/JwtSigAlgName.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using System.Diagnostics.CodeAnalysis;
-
-namespace IdentityShroud.Core;
-
-[SuppressMessage("ReSharper", "InconsistentNaming")]
-public readonly record struct JwtSigAlgName(string Name) : IEquatable
-{
- // HMAC using SHA-???
- public static JwtSigAlgName HS256 => new("HS256"); // REQUIRED
- public static JwtSigAlgName HS384 => new("HS384");
- public static JwtSigAlgName HS512 => new("HS512");
-
- // RSASSA-PKCS1-v1_5 using SHA-???
- public static JwtSigAlgName RS256 => new("RS256");
- public static JwtSigAlgName RS384 => new("RS384");
- public static JwtSigAlgName RS512 => new("RS512");
-
- public static JwtSigAlgName ES256 => new("ES256"); // ECDSA using P-256 and SHA-256
- public static JwtSigAlgName ES384 => new("ES384"); // ECDSA using P-384 and SHA-384
- public static JwtSigAlgName ES512 => new("ES512"); // ECDSA using P-521 and SHA-512
-
- public override string ToString() => Name;
-}
\ No newline at end of file
diff --git a/IdentityShroud.Core/Security/Jwt/JwtSignatureGenerator.cs b/IdentityShroud.Core/Security/Jwt/JwtSignatureGenerator.cs
deleted file mode 100644
index 99b9097..0000000
--- a/IdentityShroud.Core/Security/Jwt/JwtSignatureGenerator.cs
+++ /dev/null
@@ -1,101 +0,0 @@
-using System.Buffers.Text;
-using System.Security.Cryptography;
-using System.Text;
-using System.Text.Json;
-using IdentityShroud.Core.Model;
-using Microsoft.AspNetCore.WebUtilities;
-
-namespace IdentityShroud.Core;
-
-public static class JwtSignatureGenerator
-{
- ///
- /// Generates a JWT signature using RS256 algorithm
- ///
- /// Base64Url encoded header
- /// Base64Url encoded payload
- /// RSA private key (PEM format or RSA parameters)
- /// Base64Url encoded signature
- public static string GenerateRS256Signature(string headerBase64Url, string payloadBase64Url, RSA privateKey)
- {
- // Combine header and payload with a period
- string dataToSign = $"{headerBase64Url}.{payloadBase64Url}";
-
- // Convert to bytes
- byte[] dataBytes = Encoding.UTF8.GetBytes(dataToSign);
-
- // Sign the data using RSA-SHA256
- byte[] signatureBytes = privateKey.SignData(dataBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
-
- // Convert signature to Base64Url encoding
- string signature = WebEncoders.Base64UrlEncode(signatureBytes);
-
- return signature;
- }
-
- public static string GenerateCompleteJwt(string headerBase64Url, string payloadBase64Url, RSA privateKey)
- {
- string signature = GenerateRS256Signature(headerBase64Url, payloadBase64Url, privateKey);
- return $"{headerBase64Url}.{payloadBase64Url}.{signature}";
- }
-
-}
-
-public class JwtService(IJwtSignerFactory signerFactory)
-{
-
- public byte[] CreateEncodedJwt(ReadOnlySpan payloadUtf8, JwtSigAlgName algName, DecryptedSigningKey key)
- {
- // LATER might be able to improve performance using ArrayPool
-
- IJwtSigner signer = signerFactory.Create(algName);
- MemoryStream headerMemStream = new();
- Utf8JsonWriter headerWriter = new(headerMemStream);
- WriteJwtHeader(headerWriter, algName, key.Id.ToString());
- headerWriter.Flush();
- headerMemStream.Seek(0, SeekOrigin.Begin);
-
- int headerBase64Length = Base64Url.GetEncodedLength((int)headerMemStream.Length);
- int payloadBase64Length = Base64Url.GetEncodedLength(payloadUtf8.Length);
- var jwtData = new byte[headerBase64Length + payloadBase64Length + 1];
-
- //
- var byteArray = new byte[headerMemStream.Length];
- headerMemStream.ReadExactly(byteArray, 0, (int)headerMemStream.Length);
- int written = Base64Url.EncodeToUtf8(byteArray, jwtData);
-
- if (written != headerBase64Length)
- throw new Exception("expected header length did not match bytes written");
-
- jwtData[headerBase64Length] = (byte)'.';
-
- written = Base64Url.EncodeToUtf8(payloadUtf8, jwtData.AsSpan().Slice(headerBase64Length + 1, payloadBase64Length));
-
- if (written != payloadBase64Length)
- throw new Exception("expected payload length did not match bytes written");
-
- byte[] signature = signer.CalculateSignature(algName, key, jwtData.AsSpan());
-
- int signatureBase64Length = Base64Url.GetEncodedLength(signature.Length);
-
- var completeJwt = new byte[jwtData.Length + 1 + signatureBase64Length];
- Array.Copy(jwtData, completeJwt, jwtData.Length);
- completeJwt[jwtData.Length] = (byte)'.';
-
- written = Base64Url.EncodeToUtf8(signature, completeJwt.AsSpan().Slice(jwtData.Length + 1, signatureBase64Length));
-
- if (written != signatureBase64Length)
- throw new Exception("expected signature length did not match bytes written");
-
- return completeJwt;
- }
-
- private static void WriteJwtHeader(Utf8JsonWriter writer, JwtSigAlgName algName, string keyId)
- {
- writer.WriteStartObject();
- writer.WriteString("typ"u8, "JWT"u8);
- writer.WriteString("alg"u8, algName.ToString());
- writer.WriteString("kid"u8, keyId);
- writer.WriteEndObject();
- }
-}
\ No newline at end of file
diff --git a/IdentityShroud.Core/Security/Jwt/JwtSignerFactory.cs b/IdentityShroud.Core/Security/Jwt/JwtSignerFactory.cs
deleted file mode 100644
index 85ffe21..0000000
--- a/IdentityShroud.Core/Security/Jwt/JwtSignerFactory.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-namespace IdentityShroud.Core;
-
-public class JwtSignerFactory(IEnumerable signers) : IJwtSignerFactory
-{
- private readonly IReadOnlyDictionary _signers = signers
- .SelectMany(s => s.Algorithms.Select(alg => (alg, signer: s)))
- .ToDictionary(x => x.alg, x => x.signer);
-
- public IJwtSigner Create(JwtSigAlgName algorithm)
- {
- if (_signers.TryGetValue(algorithm, out var signer))
- return signer;
-
- throw new NotSupportedException($"JWT signing algorithm '{algorithm}' is not registered.");
- }
-
-}
\ No newline at end of file
diff --git a/IdentityShroud.Core/Security/Jwt/RsaJwtSigner.cs b/IdentityShroud.Core/Security/Jwt/RsaJwtSigner.cs
deleted file mode 100644
index 80af03c..0000000
--- a/IdentityShroud.Core/Security/Jwt/RsaJwtSigner.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-using System.Security.Cryptography;
-using IdentityShroud.Core.Model;
-
-namespace IdentityShroud.Core;
-
-public class RsaJwtSigner : IJwtSigner
-{
- public IReadOnlyList Algorithms => [JwtSigAlgName.RS256, JwtSigAlgName.RS384, JwtSigAlgName.RS512];
-
- // +-------------------+---------------------------------+
- // | "alg" Param Value | Digital Signature Algorithm |
- // +-------------------+---------------------------------+
- // | RS256 | RSASSA-PKCS1-v1_5 using SHA-256 |
- // | RS384 | RSASSA-PKCS1-v1_5 using SHA-384 |
- // | RS512 | RSASSA-PKCS1-v1_5 using SHA-512 |
- // +-------------------+---------------------------------+
-
- public byte[] CalculateSignature(JwtSigAlgName algName, DecryptedSigningKey key, ReadOnlySpan jwt)
- {
- using var rsa = RSA.Create();
- rsa.ImportPkcs8PrivateKey(key.KeyData, out int _);
- var sig = new byte[rsa.KeySize / 8];
- rsa.SignData(jwt, sig, GetHashAlgorithmName(algName), RSASignaturePadding.Pkcs1);
- return sig;
- }
-
- private static HashAlgorithmName GetHashAlgorithmName(JwtSigAlgName algName)
- => algName.Name switch
- {
- "RS256" => HashAlgorithmName.SHA256,
- "RS384" => HashAlgorithmName.SHA384,
- "RS512" => HashAlgorithmName.SHA512,
- _ => throw new ArgumentException("Invalid algorithm for RsaJwtSignatureProvider")
- };
-
-}
\ No newline at end of file
diff --git a/IdentityShroud.Core/Security/JwtSignatureGenerator.cs b/IdentityShroud.Core/Security/JwtSignatureGenerator.cs
new file mode 100644
index 0000000..11f8dc2
--- /dev/null
+++ b/IdentityShroud.Core/Security/JwtSignatureGenerator.cs
@@ -0,0 +1,38 @@
+using System.Security.Cryptography;
+using System.Text;
+using Microsoft.AspNetCore.WebUtilities;
+
+namespace IdentityShroud.Core;
+
+public class JwtSignatureGenerator
+{
+ ///
+ /// Generates a JWT signature using RS256 algorithm
+ ///
+ /// Base64Url encoded header
+ /// Base64Url encoded payload
+ /// RSA private key (PEM format or RSA parameters)
+ /// Base64Url encoded signature
+ public static string GenerateRS256Signature(string headerBase64Url, string payloadBase64Url, RSA privateKey)
+ {
+ // Combine header and payload with a period
+ string dataToSign = $"{headerBase64Url}.{payloadBase64Url}";
+
+ // Convert to bytes
+ byte[] dataBytes = Encoding.UTF8.GetBytes(dataToSign);
+
+ // Sign the data using RSA-SHA256
+ byte[] signatureBytes = privateKey.SignData(dataBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
+
+ // Convert signature to Base64Url encoding
+ string signature = WebEncoders.Base64UrlEncode(signatureBytes);
+
+ return signature;
+ }
+
+ public static string GenerateCompleteJwt(string headerBase64Url, string payloadBase64Url, RSA privateKey)
+ {
+ string signature = GenerateRS256Signature(headerBase64Url, payloadBase64Url, privateKey);
+ return $"{headerBase64Url}.{payloadBase64Url}.{signature}";
+ }
+}
\ No newline at end of file
diff --git a/IdentityShroud.Core/Security/KekId.cs b/IdentityShroud.Core/Security/KekId.cs
deleted file mode 100644
index c794078..0000000
--- a/IdentityShroud.Core/Security/KekId.cs
+++ /dev/null
@@ -1,41 +0,0 @@
-using System.ComponentModel;
-using System.Globalization;
-using System.Text.Json;
-using System.Text.Json.Serialization;
-
-namespace IdentityShroud.Core.Security;
-
-[JsonConverter(typeof(KekIdJsonConverter))]
-[TypeConverter(typeof(KekIdTypeConverter))]
-public readonly record struct KekId
-{
- public Guid Id { get; }
-
- public KekId(Guid id)
- {
- Id = id;
- }
-
- public static KekId NewId()
- {
- return new KekId(Guid.NewGuid());
- }
-}
-
-public class KekIdJsonConverter : JsonConverter
-{
- public override KekId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
- => new KekId(reader.GetGuid());
-
- public override void Write(Utf8JsonWriter writer, KekId value, JsonSerializerOptions options)
- => writer.WriteStringValue(value.Id);
-}
-
-public class KekIdTypeConverter : TypeConverter
-{
- public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
- => sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
-
- public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
- => value is string s ? new KekId(Guid.Parse(s)) : base.ConvertFrom(context, culture, value);
-}
\ No newline at end of file
diff --git a/IdentityShroud.Core/Security/KeyEncryptionKey.cs b/IdentityShroud.Core/Security/KeyEncryptionKey.cs
deleted file mode 100644
index 35f7917..0000000
--- a/IdentityShroud.Core/Security/KeyEncryptionKey.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-namespace IdentityShroud.Core.Security;
-
-///
-/// Contains a KEK and associated relevant data. This structure
-///
-///
-///
-///
-///
-public record KeyEncryptionKey(KekId Id, bool Active, string Algorithm, byte[] Key);
diff --git a/IdentityShroud.Core/Security/Keys/Aes/AesKeyPolicy.cs b/IdentityShroud.Core/Security/Keys/Aes/AesKeyPolicy.cs
deleted file mode 100644
index 5e44402..0000000
--- a/IdentityShroud.Core/Security/Keys/Aes/AesKeyPolicy.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-namespace IdentityShroud.Core.Security.Keys.Aes;
-
-public class AesKeyPolicy : KeyPolicy
-{
- public AesKeyPolicy()
- {
- KeyType = KeyType.AES;
- KeySize = 256;
- }
-}
\ No newline at end of file
diff --git a/IdentityShroud.Core/Security/Keys/Aes/AesProvider.cs b/IdentityShroud.Core/Security/Keys/Aes/AesProvider.cs
deleted file mode 100644
index b30428f..0000000
--- a/IdentityShroud.Core/Security/Keys/Aes/AesProvider.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using System.Security.Cryptography;
-using IdentityShroud.Core.Messages;
-
-namespace IdentityShroud.Core.Security.Keys.Aes;
-
-public class AesProvider : IKeyProvider
-{
- public bool IsPublic => false;
- public KeyData CreateKey(KeyPolicy policy)
- {
- return new KeyData(RandomNumberGenerator.GetBytes(policy.KeySize / 8));
- }
-
- public void SetJwkParameters(Dictionary parameters, JsonWebKey jwk)
- {
- // Can we use this for Jwe?
- throw new NotImplementedException();
- }
-}
\ No newline at end of file
diff --git a/IdentityShroud.Core/Security/Keys/IKeyProvider.cs b/IdentityShroud.Core/Security/Keys/IKeyProvider.cs
deleted file mode 100644
index 6a5ce45..0000000
--- a/IdentityShroud.Core/Security/Keys/IKeyProvider.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-using IdentityShroud.Core.Messages;
-
-namespace IdentityShroud.Core.Security.Keys;
-
-public class KeyPolicy
-{
- public KeyType KeyType { get; protected init; }
- public int KeySize { get; protected init; }
-}
-
-public record KeyData(byte[] PrivateKey, Dictionary? PublicKeyParameters = null)
-{
- ///
- /// The data to be kept private, also used for symmetric keys
- ///
- public byte[] PrivateKey { get; set; } = PrivateKey;
-
- public Dictionary? PublicKeyParameters { get; set; } = PublicKeyParameters;
-}
-
-
-public interface IKeyProvider
-{
- ///
- /// Returns true when this key uses public key cryptography
- ///
- bool IsPublic { get; }
- KeyData CreateKey(KeyPolicy policy);
-
- void SetJwkParameters(Dictionary parameters, JsonWebKey jwk);
-}
-
-
-
diff --git a/IdentityShroud.Core/Security/Keys/IKeyProviderFactory.cs b/IdentityShroud.Core/Security/Keys/IKeyProviderFactory.cs
deleted file mode 100644
index c39a836..0000000
--- a/IdentityShroud.Core/Security/Keys/IKeyProviderFactory.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace IdentityShroud.Core.Security.Keys;
-
-
-public interface IKeyProviderFactory
-{
- public IKeyProvider CreateProvider(KeyType keyType);
-}
\ No newline at end of file
diff --git a/IdentityShroud.Core/Security/Keys/KeyProviderFactory.cs b/IdentityShroud.Core/Security/Keys/KeyProviderFactory.cs
deleted file mode 100644
index 33d5092..0000000
--- a/IdentityShroud.Core/Security/Keys/KeyProviderFactory.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-using IdentityShroud.Core.Security.Keys.Aes;
-using IdentityShroud.Core.Security.Keys.Rsa;
-
-namespace IdentityShroud.Core.Security.Keys;
-
-public class KeyProviderFactory : IKeyProviderFactory
-{
- public IKeyProvider CreateProvider(KeyType keyType)
- {
- switch (keyType.Name)
- {
- case "RSA":
- return new RsaProvider();
- case "AES":
- return new AesProvider();
- default:
- throw new NotImplementedException();
- }
- }
-}
\ No newline at end of file
diff --git a/IdentityShroud.Core/Security/Keys/KeyType.cs b/IdentityShroud.Core/Security/Keys/KeyType.cs
deleted file mode 100644
index 224e989..0000000
--- a/IdentityShroud.Core/Security/Keys/KeyType.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using System.Text.Json;
-using System.Text.Json.Serialization;
-
-namespace IdentityShroud.Core.Security.Keys;
-
-[JsonConverter(typeof(KeyTypeJsonConverter))]
-public readonly record struct KeyType(string Name)
-{
- public static KeyType AES => new("AES");
- public static KeyType RSA => new("RSA");
- public override string ToString() => Name;
-}
-
-public class KeyTypeJsonConverter : JsonConverter
-{
- public override KeyType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
- => new KeyType(reader.GetString()!);
-
- public override void Write(Utf8JsonWriter writer, KeyType value, JsonSerializerOptions options)
- => writer.WriteStringValue(value.ToString());
-}
diff --git a/IdentityShroud.Core/Security/Keys/Rsa/RsaKeyPolicy.cs b/IdentityShroud.Core/Security/Keys/Rsa/RsaKeyPolicy.cs
deleted file mode 100644
index 0e2919c..0000000
--- a/IdentityShroud.Core/Security/Keys/Rsa/RsaKeyPolicy.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-namespace IdentityShroud.Core.Security.Keys.Rsa;
-
-public class RsaKeyPolicy : KeyPolicy
-{
- public RsaKeyPolicy()
- {
- KeyType = KeyType.RSA;
- KeySize = 2048;
- }
-}
\ No newline at end of file
diff --git a/IdentityShroud.Core/Security/Keys/Rsa/RsaProvider.cs b/IdentityShroud.Core/Security/Keys/Rsa/RsaProvider.cs
deleted file mode 100644
index 717f9de..0000000
--- a/IdentityShroud.Core/Security/Keys/Rsa/RsaProvider.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-using System.Buffers.Text;
-using System.Security.Cryptography;
-using IdentityShroud.Core.Messages;
-
-namespace IdentityShroud.Core.Security.Keys.Rsa;
-
-public class RsaProvider : IKeyProvider
-{
- public bool IsPublic => true;
-
- public KeyData CreateKey(KeyPolicy policy)
- {
- if (policy is RsaKeyPolicy p)
- {
- using var rsa = RSA.Create(p.KeySize);
- var publicParamaters = rsa.ExportParameters(includePrivateParameters: false);
- return new KeyData(
- rsa.ExportPkcs8PrivateKey(),
- new()
- {
- ["e"] = Base64Url.EncodeToString(publicParamaters.Exponent),
- ["n"] = Base64Url.EncodeToString(publicParamaters.Modulus),
- });
- }
-
- throw new ArgumentException("Incorrect policy type", nameof(policy));
- }
-
- public void SetJwkParameters(Dictionary parameters, JsonWebKey jwk)
- {
- jwk.Exponent = parameters["e"];
- jwk.Modulus = parameters["n"];
- }
-}
\ No newline at end of file
diff --git a/IdentityShroud.Core/Security/RsaHelper.cs b/IdentityShroud.Core/Security/RsaHelper.cs
new file mode 100644
index 0000000..9d35ad7
--- /dev/null
+++ b/IdentityShroud.Core/Security/RsaHelper.cs
@@ -0,0 +1,7 @@
+using System.Security.Cryptography;
+
+namespace IdentityShroud.Core.Security;
+
+public static class RsaHelper
+{
+}
\ No newline at end of file
diff --git a/IdentityShroud.Core/Services/ClientService.cs b/IdentityShroud.Core/Services/ClientService.cs
deleted file mode 100644
index 61be016..0000000
--- a/IdentityShroud.Core/Services/ClientService.cs
+++ /dev/null
@@ -1,80 +0,0 @@
-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),
- };
-
- }
-}
\ No newline at end of file
diff --git a/IdentityShroud.Core/Services/ClockService.cs b/IdentityShroud.Core/Services/ClockService.cs
deleted file mode 100644
index 26eb3dd..0000000
--- a/IdentityShroud.Core/Services/ClockService.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using IdentityShroud.Core.Contracts;
-
-namespace IdentityShroud.Core.Services;
-
-public class ClockService : IClock
-{
- public DateTime UtcNow()
- {
- return DateTime.UtcNow;
- }
-}
\ No newline at end of file
diff --git a/IdentityShroud.Core/Services/DataEncryptionService.cs b/IdentityShroud.Core/Services/DataEncryptionService.cs
deleted file mode 100644
index be0cf51..0000000
--- a/IdentityShroud.Core/Services/DataEncryptionService.cs
+++ /dev/null
@@ -1,47 +0,0 @@
-using System.Security.Cryptography;
-using IdentityShroud.Core.Contracts;
-using IdentityShroud.Core.Model;
-using IdentityShroud.Core.Security;
-
-namespace IdentityShroud.Core.Services;
-
-public class DataEncryptionService(
- IDekEncryptionService dekCryptor) : IDataEncryptionService
-{
- public EncryptedValue Encrypt(RealmDek dek, ReadOnlySpan plain)
- {
- Span key = stackalloc byte[dekCryptor.GetDecryptedSize(dek.KeyData)];
- try
- {
- dekCryptor.Decrypt(dek.KeyData, key);
- byte[] cipher = Encryption.Encrypt(plain, key);
- return new (dek.Id, cipher);
- }
- finally
- {
- CryptographicOperations.ZeroMemory(key);
- }
- }
-
- public byte[] Decrypt(IReadOnlyList deks, EncryptedValue input)
- {
- // Note a missing key SHOULD not happen. If it does happen something has seriously gone wrong like
- // - Old key removed before migration completed (should not be possible)
- // - Wrong keyset because of programming error.
- var dek = deks.SingleOrDefault(d => d.Id == input.DekId)
- ?? throw new InvalidOperationException("Required key not found");
-
- Span key = stackalloc byte[dekCryptor.GetDecryptedSize(dek.KeyData)];
- try
- {
- dekCryptor.Decrypt(dek.KeyData, key);
- byte[] output = new byte[Encryption.GetDecryptedLength(input.Value)];
- Encryption.Decrypt(input.Value, key, output);
- return output;
- }
- finally
- {
- CryptographicOperations.ZeroMemory(key);
- }
- }
-}
\ No newline at end of file
diff --git a/IdentityShroud.Core/Services/DekEncryptionService.cs b/IdentityShroud.Core/Services/DekEncryptionService.cs
deleted file mode 100644
index b80ea4d..0000000
--- a/IdentityShroud.Core/Services/DekEncryptionService.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-using IdentityShroud.Core.Contracts;
-using IdentityShroud.Core.Security;
-
-namespace IdentityShroud.Core.Services;
-
-///
-///
-///
-public class DekEncryptionService : IDekEncryptionService
-{
- // Note this array is expected to have one item in it most of the during key rotation it will have two
- // until it is ensured the old key can safely be removed. More then two will work but is not really expected.
- private readonly KeyEncryptionKey[] _encryptionKeys;
-
- private KeyEncryptionKey ActiveKey => _encryptionKeys.Single(k => k.Active);
- private KeyEncryptionKey GetKey(KekId keyId) => _encryptionKeys.Single(k => k.Id == keyId);
-
- public DekEncryptionService(ISecretProvider secretProvider)
- {
- _encryptionKeys = secretProvider.GetKeys("master");
- }
-
- public EncryptedDek Encrypt(ReadOnlySpan plaintext)
- {
- var encryptionKey = ActiveKey;
- byte[] cipher = Encryption.Encrypt(plaintext, encryptionKey.Key);
- return new (encryptionKey.Id, cipher);
- }
-
- public void Decrypt(EncryptedDek input, Span output)
- {
- var encryptionKey = GetKey(input.KekId);
- Encryption.Decrypt(input.Value, encryptionKey.Key, output);
- }
-
- public int GetDecryptedSize(EncryptedDek input)
- {
- return Encryption.GetDecryptedLength(input.Value);
- }
-}
\ No newline at end of file
diff --git a/IdentityShroud.Core/Services/IRealmService.cs b/IdentityShroud.Core/Services/IRealmService.cs
new file mode 100644
index 0000000..7a8ef79
--- /dev/null
+++ b/IdentityShroud.Core/Services/IRealmService.cs
@@ -0,0 +1,8 @@
+using IdentityShroud.Core.Messages.Realm;
+
+namespace IdentityShroud.Core.Services;
+
+public interface IRealmService
+{
+ Task> Create(RealmCreateRequest request, CancellationToken ct = default);
+}
\ No newline at end of file
diff --git a/IdentityShroud.Core/Services/KeyService.cs b/IdentityShroud.Core/Services/KeyService.cs
deleted file mode 100644
index 10900dd..0000000
--- a/IdentityShroud.Core/Services/KeyService.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using IdentityShroud.Core.Contracts;
-using IdentityShroud.Core.Security.Keys;
-
-namespace IdentityShroud.Core.Services;
-
-public class KeyService(
- IKeyProviderFactory keyProviderFactory) : IKeyService
-{
- public CreateKeyResponse CreateKey(KeyPolicy policy)
- {
- IKeyProvider provider = keyProviderFactory.CreateProvider(policy.KeyType);
- KeyData plainKey = provider.CreateKey(policy);
-
- return new CreateKeyResponse(policy.KeyType, plainKey);
- }
-}
diff --git a/IdentityShroud.Core/Services/MasterEncryptionService.cs b/IdentityShroud.Core/Services/MasterEncryptionService.cs
new file mode 100644
index 0000000..d0b5eda
--- /dev/null
+++ b/IdentityShroud.Core/Services/MasterEncryptionService.cs
@@ -0,0 +1,23 @@
+using IdentityShroud.Core.Contracts;
+using IdentityShroud.Core.Security;
+
+namespace IdentityShroud.Core.Services;
+
+///
+///
+///
+/// Encryption key as base64, must be 32 bytes
+public class EncryptionService(string keyBase64) : IEncryptionService
+{
+ private readonly byte[] encryptionKey = Convert.FromBase64String(keyBase64);
+
+ public byte[] Encrypt(byte[] plain)
+ {
+ return AesGcmHelper.EncryptAesGcm(plain, encryptionKey);
+ }
+
+ public byte[] Decrypt(byte[] cipher)
+ {
+ return AesGcmHelper.DecryptAesGcm(cipher, encryptionKey);
+ }
+}
\ No newline at end of file
diff --git a/IdentityShroud.Core/Services/OpenId/TokenService.cs b/IdentityShroud.Core/Services/OpenId/TokenService.cs
deleted file mode 100644
index 964b1d9..0000000
--- a/IdentityShroud.Core/Services/OpenId/TokenService.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-namespace IdentityShroud.Core.Services.OpenId;
-
-public interface ITokenService
-{
- Task> Handle(
- Dictionary form,
- string? basicAuthUser,
- string? basicAuthPassword,
- CancellationToken ct = default);
-}
-
-public class TokenService : ITokenService
-{
- public async Task> Handle(
- Dictionary form,
- string? basicAuthUser,
- string? basicAuthPassword,
- CancellationToken ct = default)
- {
- return new();
- }
-
- public async Task> ClientCredentialsFlow(
- string clientId,
- string clientSecret,
- CancellationToken ct = default)
- {
- return new();
- }
-}
\ No newline at end of file
diff --git a/IdentityShroud.Core/Services/RealmContext.cs b/IdentityShroud.Core/Services/RealmContext.cs
deleted file mode 100644
index 8c5de16..0000000
--- a/IdentityShroud.Core/Services/RealmContext.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using IdentityShroud.Core.Contracts;
-using IdentityShroud.Core.Model;
-using Microsoft.AspNetCore.Http;
-
-namespace IdentityShroud.Core.Services;
-
-public class RealmContext(
- IHttpContextAccessor accessor,
- IRealmService realmService) : IRealmContext
-{
- public Realm GetRealm()
- {
- return (Realm)accessor.HttpContext.Items["RealmEntity"];
- }
-
- public async Task> GetDeks(CancellationToken ct = default)
- {
- Realm realm = GetRealm();
- if (realm.DataEncryptionKeys.Count == 0)
- {
- await realmService.LoadDeks(realm);
- }
-
- return realm.DataEncryptionKeys;
- }
-}
\ No newline at end of file
diff --git a/IdentityShroud.Core/Services/RealmService.cs b/IdentityShroud.Core/Services/RealmService.cs
index 9dd3ba8..50cb61d 100644
--- a/IdentityShroud.Core/Services/RealmService.cs
+++ b/IdentityShroud.Core/Services/RealmService.cs
@@ -1,35 +1,18 @@
+using System.Security.Cryptography;
using IdentityShroud.Core.Contracts;
-using IdentityShroud.Core.EFCore;
using IdentityShroud.Core.Helpers;
using IdentityShroud.Core.Messages.Realm;
using IdentityShroud.Core.Model;
-using IdentityShroud.Core.Security;
-using IdentityShroud.Core.Security.Keys;
-using IdentityShroud.Core.Security.Keys.Aes;
-using IdentityShroud.Core.Security.Keys.Rsa;
-using Microsoft.EntityFrameworkCore;
namespace IdentityShroud.Core.Services;
+public record RealmCreateResponse(Guid Id, string Slug, string Name);
+
public class RealmService(
Db db,
- IKeyService keyService,
- IDekEncryptionService dekCryptor,
- IClock clock) : IRealmService
+ IEncryptionService encryptionService) : IRealmService
{
- public async Task FindById(Guid id, CancellationToken ct = default)
- {
- return await db.Realms
- .SingleOrDefaultAsync(r => r.Id == id, ct);
- }
-
- public async Task FindBySlug(string slug, CancellationToken ct = default)
- {
- return await db.Realms
- .SingleOrDefaultAsync(r => r.Slug == slug, ct);
- }
-
- public async Task> Create(RealmCreateRequest request, CancellationToken ct = default)
+ public async Task> Create(RealmCreateRequest request, CancellationToken ct = default)
{
Realm realm = new()
{
@@ -38,61 +21,13 @@ public class RealmService(
Name = request.Name,
};
- realm.TokenSigningKeys.Add(CreateSigningKey(realm));
- realm.DataEncryptionKeys.Add(CreateDataEncryptionKey(realm));
+ using RSA rsa = RSA.Create(2048);
+ realm.SetPrivateKey(encryptionService, rsa.ExportPkcs8PrivateKey());
db.Add(realm);
await db.SaveChangesAsync(ct);
-
- return realm;
- }
-
- private RealmSigningKey CreateSigningKey(Realm realm)
- {
- var k = keyService.CreateKey(GetSigningKeyPolicy(realm));
- return new RealmSigningKey
- {
- Id = RealmSigningKeyId.NewId(),
- KeyType = k.KeyType,
- Key = dekCryptor.Encrypt(k.Key.PrivateKey),
- PublicKeyParameters = k.Key.PublicKeyParameters,
- CreatedAt = clock.UtcNow(),
- };
- }
-
- private RealmDek CreateDataEncryptionKey(Realm realm)
- {
- var k = keyService.CreateKey(GetDataKeyPolicy(realm));
- return new RealmDek()
- {
- Id = DekId.NewId(),
- Active = true,
- Algorithm = k.KeyType,
- KeyData = dekCryptor.Encrypt(k.Key.PrivateKey),
- };
- }
-
-
- ///
- /// Place holder for getting policies from the realm and falling back to sane defaults when no policies have been set.
- ///
- ///
- ///
- private KeyPolicy GetSigningKeyPolicy(Realm _) => new RsaKeyPolicy();
- private KeyPolicy GetDataKeyPolicy(Realm _) => new AesKeyPolicy();
-
- public async Task LoadActiveKeys(Realm realm)
- {
- await db.Entry(realm).Collection(r => r.TokenSigningKeys)
- .Query()
- .Where(k => k.RevokedAt == null)
- .LoadAsync();
- }
-
- public async Task LoadDeks(Realm realm)
- {
- await db.Entry(realm).Collection(r => r.DataEncryptionKeys)
- .Query()
- .LoadAsync();
+
+ return new RealmCreateResponse(
+ realm.Id, realm.Slug, realm.Name);
}
}
\ No newline at end of file
diff --git a/IdentityShroud.GraphQL/IdentityShroud.GraphQL.csproj b/IdentityShroud.GraphQL/IdentityShroud.GraphQL.csproj
deleted file mode 100644
index 1ba525f..0000000
--- a/IdentityShroud.GraphQL/IdentityShroud.GraphQL.csproj
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
- net10.0
- enable
- enable
-
-
-
-
-
-
-
-
-
-
-
diff --git a/IdentityShroud.GraphQL/Query.cs b/IdentityShroud.GraphQL/Query.cs
deleted file mode 100644
index 5ccbeb1..0000000
--- a/IdentityShroud.GraphQL/Query.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-susing IdentityShroud.Core.Contracts;
-using IdentityShroud.Core.Model;
-
-namespace IdentityShroud.GraphQL;
-
-public class Query
-{
- public string GetHello() => "Hello, world!";
-
- public async Task GetRealms(
- Guid id,
- [Service] IRealmService realmService)
- {
- return await realmService.FindById(id);
- }
-}
-
-public class Mutation
-{
- public async Task RealmCreate(string name)
- {
- Realm r = new();
-
- return r;
- }
-}
\ No newline at end of file
diff --git a/IdentityShroud.GraphQL/RegistrationExtensions.cs b/IdentityShroud.GraphQL/RegistrationExtensions.cs
deleted file mode 100644
index dad3056..0000000
--- a/IdentityShroud.GraphQL/RegistrationExtensions.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-using Microsoft.AspNetCore.Builder;
-using Microsoft.AspNetCore.Routing;
-using Microsoft.Extensions.DependencyInjection;
-
-namespace IdentityShroud.GraphQL;
-
-public static class RegistrationExtensions
-{
- extension(IServiceCollection services)
- {
- public IServiceCollection AddIdentityShroudGraphQL()
- {
- services
- .AddGraphQLServer()
- .AddMutationConventions(applyToAllMutations: true)
- .AddMutationType()
- .AddQueryType();
-
- return services;
- }
- }
-
- extension(IEndpointRouteBuilder app)
- {
- public IEndpointRouteBuilder MapIdentityShroudGraphQL()
- {
- app.MapGraphQL();
- return app;
- }
- }
-}
\ No newline at end of file
diff --git a/IdentityShroud.Migrations/DesignTimeDbFactory.cs b/IdentityShroud.Migrations/DesignTimeDbFactory.cs
index e03d3ef..9459610 100644
--- a/IdentityShroud.Migrations/DesignTimeDbFactory.cs
+++ b/IdentityShroud.Migrations/DesignTimeDbFactory.cs
@@ -1,4 +1,4 @@
-using IdentityShroud.Core.EFCore;
+using IdentityShroud.Core;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
diff --git a/IdentityShroud.Migrations/IdentityShroud.Migrations.csproj b/IdentityShroud.Migrations/IdentityShroud.Migrations.csproj
index 8cc28ca..f4583e2 100644
--- a/IdentityShroud.Migrations/IdentityShroud.Migrations.csproj
+++ b/IdentityShroud.Migrations/IdentityShroud.Migrations.csproj
@@ -1,4 +1,4 @@
-
+
net10.0
@@ -7,7 +7,7 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/IdentityShroud.Migrations/Migrations/20260412083710_Initial.Designer.cs b/IdentityShroud.Migrations/Migrations/20260412083710_Initial.Designer.cs
deleted file mode 100644
index 6c3df6d..0000000
--- a/IdentityShroud.Migrations/Migrations/20260412083710_Initial.Designer.cs
+++ /dev/null
@@ -1,318 +0,0 @@
-//
-using System;
-using System.Collections.Generic;
-using IdentityShroud.Core.EFCore;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Migrations;
-using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
-using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
-
-#nullable disable
-
-namespace IdentityShroud.Migrations.Migrations
-{
- [DbContext(typeof(Db))]
- [Migration("20260412083710_Initial")]
- partial class Initial
- {
- ///
- protected override void BuildTargetModel(ModelBuilder modelBuilder)
- {
-#pragma warning disable 612, 618
- modelBuilder
- .HasAnnotation("ProductVersion", "10.0.2")
- .HasAnnotation("Relational:MaxIdentifierLength", 63);
-
- NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
-
- modelBuilder.Entity("IdentityShroud.Core.Model.Client", b =>
- {
- b.Property("Id")
- .ValueGeneratedOnAdd()
- .HasColumnType("integer")
- .HasColumnName("id");
-
- NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
-
- b.Property("AllowClientCredentialsFlow")
- .HasColumnType("boolean")
- .HasColumnName("allow_client_credentials_flow");
-
- b.Property("ClientId")
- .IsRequired()
- .HasMaxLength(40)
- .HasColumnType("character varying(40)")
- .HasColumnName("client_id");
-
- b.Property("Confidential")
- .HasColumnType("boolean")
- .HasColumnName("confidential");
-
- b.Property("CreatedAt")
- .HasColumnType("timestamp with time zone")
- .HasColumnName("created_at");
-
- b.Property("Description")
- .HasMaxLength(2048)
- .HasColumnType("character varying(2048)")
- .HasColumnName("description");
-
- b.Property("Name")
- .HasMaxLength(80)
- .HasColumnType("character varying(80)")
- .HasColumnName("name");
-
- b.Property("RealmId")
- .HasColumnType("uuid")
- .HasColumnName("realm_id");
-
- b.Property("SignatureAlgorithm")
- .HasMaxLength(20)
- .HasColumnType("character varying(20)")
- .HasColumnName("signature_algorithm");
-
- b.HasKey("Id")
- .HasName("pk_client");
-
- b.HasIndex("ClientId")
- .IsUnique()
- .HasDatabaseName("ix_client_client_id");
-
- b.HasIndex("RealmId")
- .HasDatabaseName("ix_client_realm_id");
-
- b.ToTable("client", (string)null);
- });
-
- modelBuilder.Entity("IdentityShroud.Core.Model.ClientSecret", b =>
- {
- b.Property("Id")
- .ValueGeneratedOnAdd()
- .HasColumnType("integer")
- .HasColumnName("id");
-
- NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
-
- b.Property("ClientId")
- .HasColumnType("uuid")
- .HasColumnName("client_id");
-
- b.Property("ClientId1")
- .HasColumnType("integer")
- .HasColumnName("client_id1");
-
- b.Property("CreatedAt")
- .HasColumnType("timestamp with time zone")
- .HasColumnName("created_at");
-
- b.Property("Expires")
- .HasColumnType("timestamp with time zone")
- .HasColumnName("expires");
-
- b.Property("RevokedAt")
- .HasColumnType("timestamp with time zone")
- .HasColumnName("revoked_at");
-
- b.ComplexProperty(typeof(Dictionary), "Secret", "IdentityShroud.Core.Model.ClientSecret.Secret#EncryptedValue", b1 =>
- {
- b1.IsRequired();
-
- b1.Property("DekId")
- .HasColumnType("uuid")
- .HasColumnName("secret_dek_id");
-
- b1.Property("Value")
- .IsRequired()
- .HasColumnType("bytea")
- .HasColumnName("secret_value");
- });
-
- b.HasKey("Id")
- .HasName("pk_client_secret");
-
- b.HasIndex("ClientId1")
- .HasDatabaseName("ix_client_secret_client_id1");
-
- b.ToTable("client_secret", (string)null);
- });
-
- modelBuilder.Entity("IdentityShroud.Core.Model.Realm", b =>
- {
- b.Property("Id")
- .ValueGeneratedOnAdd()
- .HasColumnType("uuid")
- .HasColumnName("id");
-
- b.Property("DefaultSignatureAlgorithm")
- .IsRequired()
- .HasColumnType("text")
- .HasColumnName("default_signature_algorithm");
-
- b.Property("Name")
- .IsRequired()
- .HasMaxLength(128)
- .HasColumnType("character varying(128)")
- .HasColumnName("name");
-
- b.Property