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..8d08a27 100644 --- a/IdentityShroud.Api.Tests/Apis/RealmApisTests.cs +++ b/IdentityShroud.Api.Tests/Apis/RealmApisTests.cs @@ -1,13 +1,14 @@ -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; +using IdentityShroud.Core.Contracts; using IdentityShroud.Core.Model; using IdentityShroud.Core.Tests.Fixtures; using IdentityShroud.TestUtils.Asserts; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.WebUtilities; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; @@ -113,7 +114,7 @@ public class RealmApisTests : IClassFixture { // act var client = _factory.CreateClient(); - var response = await client.GetAsync($"/realms/{slug}/.well-known/openid-configuration", + var response = await client.GetAsync("/realms/bar/.well-known/openid-configuration", TestContext.Current.CancellationToken); // verify @@ -123,16 +124,26 @@ public class RealmApisTests : IClassFixture [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); + // setup + IEncryptionService encryptionService = _factory.Services.GetRequiredService(); + + using var rsa = RSA.Create(2048); + RSAParameters parameters = rsa.ExportParameters(includePrivateParameters: false); + + RealmKey realmKey = new( + Guid.NewGuid(), + "RSA", + encryptionService.Encrypt(rsa.ExportPkcs8PrivateKey()), + DateTime.UtcNow); + + await ScopedContextAsync(async db => + { + db.Realms.Add(new Realm() { Slug = "foo", Name = "Foo", Keys = [ realmKey ]}); + await db.SaveChangesAsync(TestContext.Current.CancellationToken); + }); // act + var client = _factory.CreateClient(); var response = await client.GetAsync("/auth/realms/foo/openid-connect/jwks", TestContext.Current.CancellationToken); @@ -140,16 +151,9 @@ public class RealmApisTests : IClassFixture 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); + JsonObjectAssert.Equal(realmKey.Id.ToString(), payload, "keys[0].kid"); + JsonObjectAssert.Equal(WebEncoders.Base64UrlEncode(parameters.Modulus!), payload, "keys[0].n"); + JsonObjectAssert.Equal(WebEncoders.Base64UrlEncode(parameters.Exponent!), payload, "keys[0].e"); } private async Task ScopedContextAsync( @@ -160,22 +164,4 @@ public class RealmApisTests : IClassFixture 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..6f4c461 100644 --- a/IdentityShroud.Api.Tests/Fixtures/ApplicationFactory.cs +++ b/IdentityShroud.Api.Tests/Fixtures/ApplicationFactory.cs @@ -1,7 +1,11 @@ -using IdentityShroud.Api; +using IdentityShroud.Core.Services; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.VisualStudio.TestPlatform.TestHost; +using Npgsql; using Testcontainers.PostgreSql; namespace IdentityShroud.Core.Tests.Fixtures; @@ -29,10 +33,7 @@ public class ApplicationFactory : WebApplicationFactory, IAsyncLifetime 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=", + ["Encryption:Master"] = "GVd07qW0frRX9quPX/X62L88BeRR7+IzgRJHtG7ZzHw=", }); }); 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..a3aa6a8 100644 --- a/IdentityShroud.Api.Tests/IdentityShroud.Api.Tests.csproj +++ b/IdentityShroud.Api.Tests/IdentityShroud.Api.Tests.csproj @@ -1,4 +1,4 @@ - + net10.0 @@ -8,22 +8,22 @@ - - - - - - - - - + + + + + + + + + - - + + - + diff --git a/IdentityShroud.Api.Tests/Mappers/KeyMapperTests.cs b/IdentityShroud.Api.Tests/Mappers/KeyMapperTests.cs new file mode 100644 index 0000000..767337e --- /dev/null +++ b/IdentityShroud.Api.Tests/Mappers/KeyMapperTests.cs @@ -0,0 +1,17 @@ +using IdentityShroud.Api.Mappers; +using IdentityShroud.Core.Contracts; +using IdentityShroud.Core.Messages; +using IdentityShroud.TestUtils.Substitutes; +using Microsoft.AspNetCore.WebUtilities; + +namespace IdentityShroud.Api.Tests.Mappers; + +// public class KeyMapperTests +// { +// private readonly IEncryptionService _encryptionService = EncryptionServiceSubstitute.CreatePassthrough(); +// +// [Fact] +// public void Test() +// { +// } +// } \ No newline at end of file diff --git a/IdentityShroud.Api.Tests/Mappers/KeyServiceTests.cs b/IdentityShroud.Api.Tests/Mappers/KeyServiceTests.cs new file mode 100644 index 0000000..196b15d --- /dev/null +++ b/IdentityShroud.Api.Tests/Mappers/KeyServiceTests.cs @@ -0,0 +1,43 @@ +using System.Buffers.Text; +using System.Security.Cryptography; +using IdentityShroud.Core.Contracts; +using IdentityShroud.Core.Model; +using IdentityShroud.Core.Security.Keys; +using IdentityShroud.Core.Services; +using IdentityShroud.TestUtils.Substitutes; + +namespace IdentityShroud.Api.Tests.Mappers; + +public class KeyServiceTests +{ + private readonly IEncryptionService _encryptionService = EncryptionServiceSubstitute.CreatePassthrough(); + //private readonly IKeyProviderFactory _keyProviderFactory = Substitute.For(); + + [Fact] + public void Test() + { + // Setup + using RSA rsa = RSA.Create(2048); + + RSAParameters parameters = rsa.ExportParameters(includePrivateParameters: false); + + RealmKey realmKey = new( + new("60bb79cf-4bac-4521-87f2-ac87cc15541f"), + "RSA", + rsa.ExportPkcs8PrivateKey(), + DateTime.UtcNow) + { + Priority = 10, + }; + + // Act + KeyService sut = new(_encryptionService, new KeyProviderFactory(), new ClockService()); + var jwk = sut.CreateJsonWebKey(realmKey); + + Assert.Equal("RSA", jwk.KeyType); + Assert.Equal(realmKey.Id.ToString(), jwk.KeyId); + Assert.Equal("sig", jwk.Use); + Assert.Equal(parameters.Exponent, Base64Url.DecodeFromChars(jwk.Exponent)); + Assert.Equal(parameters.Modulus, Base64Url.DecodeFromChars(jwk.Modulus)); + } +} \ No newline at end of file diff --git a/IdentityShroud.Api/Apis/ClientApi.cs b/IdentityShroud.Api/Apis/ClientApi.cs index 05b4aa7..fd3e804 100644 --- a/IdentityShroud.Api/Apis/ClientApi.cs +++ b/IdentityShroud.Api/Apis/ClientApi.cs @@ -1,11 +1,15 @@ using FluentResults; -using IdentityShroud.Api.Mappers; using IdentityShroud.Core.Contracts; +using IdentityShroud.Core.Messages.Realm; using IdentityShroud.Core.Model; +using IdentityShroud.Core.Services; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; -namespace IdentityShroud.Api.Apis; +namespace IdentityShroud.Api; + + +public record ClientCreateReponse(int Id, string ClientId); /// /// The part of the api below realms/{slug}/clients @@ -16,12 +20,12 @@ public static class ClientApi public static void MapEndpoints(this IEndpointRouteBuilder erp) { - RouteGroupBuilder clientsGroup = erp.MapGroup("clients"); - + RouteGroupBuilder clientsGroup = erp.MapGroup("clients"); + clientsGroup.MapPost("", ClientCreate) - .Produces(StatusCodes.Status201Created) - .Validate() - .WithName("ClientCreate"); + .Validate() + .WithName("ClientCreate") + .Produces(StatusCodes.Status201Created); var clientIdGroup = clientsGroup.MapGroup("{clientId}") .AddEndpointFilter(); @@ -30,21 +34,15 @@ public static class ClientApi .WithName(ClientGetRouteName); } - private static Ok ClientGet( - Guid realmId, - int clientId, - HttpContext context) + private static Task ClientGet(HttpContext context) { - Client client = (Client)context.Items["ClientEntity"]!; - return TypedResults.Ok(new ClientMapper().ToDto(client)); + throw new NotImplementedException(); } - private static async Task, InternalServerError>> + private static async Task, InternalServerError>> ClientCreate( - Guid realmId, ClientCreateRequest request, [FromServices] IClientService service, - [FromServices] IDataEncryptionService cryptor, HttpContext context, CancellationToken cancellationToken) { @@ -57,41 +55,15 @@ public static class ClientApi } 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, + new ClientCreateReponse(client.Id, client.ClientId), 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; + throw new NotImplementedException(); } } \ 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 index 5e2590f..3c47b48 100644 --- a/IdentityShroud.Api/Apis/EndpointRouteBuilderExtensions.cs +++ b/IdentityShroud.Api/Apis/EndpointRouteBuilderExtensions.cs @@ -2,10 +2,9 @@ namespace IdentityShroud.Api; public static class EndpointRouteBuilderExtensions { - public static IEndpointConventionBuilder Validate(this IEndpointConventionBuilder builder) - where TDto : class - => builder.AddEndpointFilter>(); - + public static RouteHandlerBuilder Validate(this RouteHandlerBuilder builder) where TDto : class + => builder.AddEndpointFilter>(); + public static void MapApis(this IEndpointRouteBuilder erp) { RealmApi.MapRealmEndpoints(erp); diff --git a/IdentityShroud.Api/Apis/Filters/ClientIdValidationFilter.cs b/IdentityShroud.Api/Apis/Filters/ClientIdValidationFilter.cs index 771be81..8030153 100644 --- a/IdentityShroud.Api/Apis/Filters/ClientIdValidationFilter.cs +++ b/IdentityShroud.Api/Apis/Filters/ClientIdValidationFilter.cs @@ -7,9 +7,8 @@ public class ClientIdValidationFilter(IClientService clientService) : IEndpointF { public async ValueTask InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) { - Guid realmId = context.Arguments.OfType().First(); int id = context.Arguments.OfType().First(); - Client? client = await clientService.FindById(realmId, id, context.HttpContext.RequestAborted); + Client? client = await clientService.FindById(id, context.HttpContext.RequestAborted); if (client is null) { return Results.NotFound(); diff --git a/IdentityShroud.Api/Apis/Filters/RealmSlugValidationFilter.cs b/IdentityShroud.Api/Apis/Filters/RealmSlugValidationFilter.cs index 75338e1..862b599 100644 --- a/IdentityShroud.Api/Apis/Filters/RealmSlugValidationFilter.cs +++ b/IdentityShroud.Api/Apis/Filters/RealmSlugValidationFilter.cs @@ -1,5 +1,6 @@ using IdentityShroud.Core.Contracts; using IdentityShroud.Core.Model; +using IdentityShroud.Core.Services; namespace IdentityShroud.Api; diff --git a/IdentityShroud.Api/Apis/Helpers/HeaderHelpers.cs b/IdentityShroud.Api/Apis/Helpers/HeaderHelpers.cs deleted file mode 100644 index 35f7f30..0000000 --- a/IdentityShroud.Api/Apis/Helpers/HeaderHelpers.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Text; -using Microsoft.Extensions.Primitives; - -namespace IdentityShroud.Api.Helpers; - -public static class HeaderHelpers -{ - public static bool TryGetBasicAuth( - HttpContext context, - [NotNullWhen(true)] out string? user, - [NotNullWhen(true)] out string? password) - { - var headers = context?.Request.Headers; - if (headers is not null) - { - if (headers.TryGetValue("Authorization", out StringValues s)) - return TryDecodeBasicAuth(s.ToString(), out user, out password); - } - - user = password = null; - return false; - } - - public static bool TryDecodeBasicAuth( - string authorizationHeader, - [NotNullWhen(true)] out string? user, - [NotNullWhen(true)] out string? password) - { - if (authorizationHeader.StartsWith("basic ", StringComparison.OrdinalIgnoreCase)) - { - ReadOnlySpan val = authorizationHeader.AsSpan(6); // basic + space - Span b = new byte[(val.Length * 6 / 8) + 1]; - if (Convert.TryFromBase64Chars(val, b, out int written)) - { - int sepIdx = b.IndexOf((byte)':'); - if (sepIdx > 0 && sepIdx < written - 1) - { - user = Encoding.UTF8.GetString(b.Slice(0, sepIdx)); - password = Encoding.UTF8.GetString(b.Slice(sepIdx + 1, written - (sepIdx + 1))); - return true; - } - } - } - - user = password = null; - return false; - } - -} \ No newline at end of file diff --git a/IdentityShroud.Api/Apis/ISResults/ISUnauthorizedHttpResult.cs b/IdentityShroud.Api/Apis/ISResults/ISUnauthorizedHttpResult.cs deleted file mode 100644 index f7722cb..0000000 --- a/IdentityShroud.Api/Apis/ISResults/ISUnauthorizedHttpResult.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace IdentityShroud.Api.Apis.ISResults; - -public class ISUnauthorizedHttpResult : IResult, IStatusCodeHttpResult -{ - private readonly List _wwwAuthenticateValues; - /// - /// Initializes a new instance of the class. - /// - internal ISUnauthorizedHttpResult(List wwwAuthenticateValues) - { - _wwwAuthenticateValues = wwwAuthenticateValues; - } - - /// - /// Gets the HTTP status code: - /// - public int StatusCode => StatusCodes.Status401Unauthorized; - - int? IStatusCodeHttpResult.StatusCode => StatusCode; - - /// - public Task ExecuteAsync(HttpContext httpContext) - { - ArgumentNullException.ThrowIfNull(httpContext); - - // Creating the logger with a string to preserve the category after the refactoring. - // var loggerFactory = httpContext.RequestServices.GetRequiredService(); - // var logger = loggerFactory.CreateLogger("IdentityShroud.Api.Results.ISUnauthorizedResult"); - // HttpResultsHelper.Log.WritingResultAsStatusCode(logger, StatusCode); - - - httpContext.Response.Headers.WWWAuthenticate = new(_wwwAuthenticateValues.ToArray()); - - httpContext.Response.StatusCode = StatusCode; - - return Task.CompletedTask; - } -} \ No newline at end of file diff --git a/IdentityShroud.Api/Apis/Mappers/ClientMapper.cs b/IdentityShroud.Api/Apis/Mappers/ClientMapper.cs deleted file mode 100644 index 0c6563f..0000000 --- a/IdentityShroud.Api/Apis/Mappers/ClientMapper.cs +++ /dev/null @@ -1,14 +0,0 @@ -using IdentityShroud.Core.Model; -using Riok.Mapperly.Abstractions; - -namespace IdentityShroud.Api.Mappers; - -[Mapper] -public partial class ClientMapper -{ - // skipping secret as we do not have the DEK - [MapperIgnoreSource(nameof(Client.Secrets))] - [MapperIgnoreTarget(nameof(ClientRepresentation.Secret))] - public partial ClientRepresentation ToDto(Client client); - -} \ No newline at end of file diff --git a/IdentityShroud.Api/Apis/Mappers/KeyMapper.cs b/IdentityShroud.Api/Apis/Mappers/KeyMapper.cs index e37798b..36bd200 100644 --- a/IdentityShroud.Api/Apis/Mappers/KeyMapper.cs +++ b/IdentityShroud.Api/Apis/Mappers/KeyMapper.cs @@ -1,28 +1,23 @@ +using System.Security.Cryptography; +using IdentityShroud.Core.Contracts; using IdentityShroud.Core.Messages; using IdentityShroud.Core.Model; -using IdentityShroud.Core.Security.Keys; +using IdentityShroud.Core.Security; +using Microsoft.AspNetCore.WebUtilities; namespace IdentityShroud.Api.Mappers; -public class KeyMapper(IKeyProviderFactory keyProviderFactory) +public class KeyMapper(IKeyService keyService) { - public JsonWebKeySet KeyListToJsonWebKeySet(IEnumerable keys) + public JsonWebKeySet KeyListToJsonWebKeySet(IEnumerable keys) { JsonWebKeySet wks = new(); foreach (var k in keys) { - IKeyProvider provider = keyProviderFactory.CreateProvider(k.KeyType); - if (provider.IsPublic) + var wk = keyService.CreateJsonWebKey(k); + if (wk is {}) { - JsonWebKey jwk = new() - { - KeyId = k.Id.ToString(), - KeyType = k.KeyType, - Use = "sig", - }; - - provider.SetJwkParameters(k.PublicKeyParameters!, jwk); - wks.Keys.Add(jwk); + wks.Keys.Add(wk); } } return wks; diff --git a/IdentityShroud.Api/Apis/OpenIdEndpoints.cs b/IdentityShroud.Api/Apis/OpenIdEndpoints.cs index 54b972a..6565413 100644 --- a/IdentityShroud.Api/Apis/OpenIdEndpoints.cs +++ b/IdentityShroud.Api/Apis/OpenIdEndpoints.cs @@ -1,11 +1,7 @@ -using IdentityShroud.Api.Apis; -using IdentityShroud.Api.Apis.ISResults; -using IdentityShroud.Api.Helpers; using IdentityShroud.Api.Mappers; using IdentityShroud.Core.Contracts; using IdentityShroud.Core.Messages; using IdentityShroud.Core.Model; -using IdentityShroud.Core.Services.OpenId; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; @@ -15,6 +11,8 @@ public static class OpenIdEndpoints { // openid: auth/realms/{realmSlug}/.well-known/openid-configuration // openid: auth/realms/{realmSlug}/openid-connect/(auth|token|jwks) + + public static void MapEndpoints(this IEndpointRouteBuilder erp) { var realmsGroup = erp.MapGroup("/auth/realms"); @@ -47,7 +45,7 @@ public static class OpenIdEndpoints TokenEndpoint = baseUri + "/openid-connect/token", Issuer = baseUri, JwksUri = baseUri + "/openid-connect/jwks", - }); + }, AppJsonSerializerContext.Default.OpenIdConfiguration); } private static async Task, BadRequest>> OpenIdConnectJwks( @@ -58,79 +56,17 @@ public static class OpenIdEndpoints { Realm realm = context.GetValidatedRealm(); await realmService.LoadActiveKeys(realm); - return TypedResults.Ok(keyMapper.KeyListToJsonWebKeySet(realm.TokenSigningKeys)); + return TypedResults.Ok(keyMapper.KeyListToJsonWebKeySet(realm.Keys)); } - private static async Task, - BadRequest, - ISUnauthorizedHttpResult - >> OpenIdConnectToken( - string realmSlug, - [FromServices] IClientService clientService, - HttpContext context, - CancellationToken ct) + private static Task OpenIdConnectToken(HttpContext context) { - IFormCollection form = await context.Request.ReadFormAsync(); - - string grantType = form["grant_type"].ToString(); - string clientId = form["client_id"].ToString(); - string scope = form["scope"].ToString(); - - if (grantType == "client_credentials") - { - string? clientSecret = null; - bool withAuthHeader = false; - if (HeaderHelpers.TryGetBasicAuth(context, out string? user, out string? password)) - { - withAuthHeader = true; - clientId = user; - clientSecret = password; - } - clientSecret ??= form["client_secret"].ToString(); - - if (string.IsNullOrEmpty(clientId) || - string.IsNullOrEmpty(clientSecret)) - { - return CreateBadRequest("invalid_request"); - } - - Realm realm = context.GetValidatedRealm(); - Client? client = await clientService.GetByClientId(realm.Id, clientId, ct); - if (client is null) - { - if (withAuthHeader) - { - return new ISUnauthorizedHttpResult([$"Basic realm=\"{realm.Slug}\""]); - } - return CreateBadRequest("invalid_client"); - } - - if (!client.AllowClientCredentialsFlow) - return CreateBadRequest("unauthorized_client"); - - } - else - return CreateBadRequest("unsupported_grant_type"); - - context.Response.Headers.CacheControl = "no-store"; - context.Response.Headers.Pragma = "no-cache"; - - return TypedResults.Ok(new TokenResponse() - { - AccessToken = "token", - TokenType = "bearer", - ExpiresIn = 3600, - }); + throw new NotImplementedException(); } - private static BadRequest CreateBadRequest(string error) => - TypedResults.BadRequest(new ErrorDto(error)); - - - private static Task OpenIdConnectAuth(HttpContext context) { throw new NotImplementedException(); } + } \ No newline at end of file diff --git a/IdentityShroud.Api/Apis/RealmApi.cs b/IdentityShroud.Api/Apis/RealmApi.cs index ed78cef..88a5179 100644 --- a/IdentityShroud.Api/Apis/RealmApi.cs +++ b/IdentityShroud.Api/Apis/RealmApi.cs @@ -1,7 +1,7 @@ -using IdentityShroud.Api.Apis; using IdentityShroud.Core.Contracts; using IdentityShroud.Core.Messages.Realm; using IdentityShroud.Core.Model; +using IdentityShroud.Core.Services; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; @@ -19,56 +19,31 @@ public static class HttpContextExtensions public static class RealmApi { - public const string GetRealmRoute = "Get Realm"; - public const string CreateRealmRoute = "Create Realm"; - public static void MapRealmEndpoints(IEndpointRouteBuilder erp) { var realmsGroup = erp.MapGroup("/api/v1/realms"); - realmsGroup.MapPost("", RealmCreate) - .Produces(StatusCodes.Status201Created) - .Validate() - .WithName(CreateRealmRoute); - + .Validate() + .WithName("Create Realm") + .Produces(StatusCodes.Status201Created); var realmIdGroup = realmsGroup.MapGroup("{realmId}") .AddEndpointFilter(); - realmIdGroup.MapGet("", RealmGet) - .WithName(GetRealmRoute); - ClientApi.MapEndpoints(realmIdGroup); - } + + - private static Ok RealmGet( - Guid realmId, - HttpContext context) - { - Realm realm = context.GetValidatedRealm(); - return TypedResults.Ok(MapToRepresentation(realm)); } - - private static async Task, InternalServerError>> + + private static async Task, InternalServerError>> RealmCreate(RealmCreateRequest request, [FromServices] IRealmService service) { var response = await service.Create(request); if (response.IsSuccess) - { - var realm = response.Value; - return TypedResults.CreatedAtRoute( - MapToRepresentation(realm), - GetRealmRoute, - new { realmId = realm.Id }); - } - + return TypedResults.Created($"/realms/{response.Value.Slug}", response.Value); + // TODO make helper to convert failure response to a proper HTTP result. return TypedResults.InternalServerError(); } - - private static RealmRepresentation MapToRepresentation(Realm realm) - => new(realm.Id, realm.Slug, realm.Name); -} - - - +} \ No newline at end of file diff --git a/IdentityShroud.Api/Apis/Validation/ClientCreateRequestValidator.cs b/IdentityShroud.Api/Apis/Validation/ClientCreateRequestValidator.cs deleted file mode 100644 index aef7c47..0000000 --- a/IdentityShroud.Api/Apis/Validation/ClientCreateRequestValidator.cs +++ /dev/null @@ -1,24 +0,0 @@ -using FluentValidation; -using IdentityShroud.Core.Contracts; - -namespace IdentityShroud.Api; - -public class ClientCreateRequestValidator : AbstractValidator -{ - // most of standard ascii minus the control characters and space - private const string ClientIdPattern = "^[a-zA-Z0-9_-]+"; - - private readonly string[] _allowedAlgorithms = [ "RS256", "ES256" ]; - - public ClientCreateRequestValidator() - { - RuleFor(e => e.ClientId).NotEmpty().MaximumLength(40).Matches(ClientIdPattern); - RuleFor(e => e.Name).MaximumLength(80); - RuleFor(e => e.Description).MaximumLength(2048); - RuleFor(e => e.SignatureAlgorithm) - .Must(v => v is null || _allowedAlgorithms.Contains(v)) - .WithMessage($"SignatureAlgorithm must be one of {string.Join(", ", _allowedAlgorithms)} or null"); - RuleFor(e => e.AllowClientCredentialsFlow).Must(v => v is not true).When(e => e.Confidential is not true); - RuleFor(e => e.GenerateSecret).Must(v => v is not true).When(e => e.Confidential is not true); - } -} \ No newline at end of file diff --git a/IdentityShroud.Api/Apis/Filters/ValidateFilter.cs b/IdentityShroud.Api/Apis/Validation/ValidateFilter.cs similarity index 100% rename from IdentityShroud.Api/Apis/Filters/ValidateFilter.cs rename to IdentityShroud.Api/Apis/Validation/ValidateFilter.cs diff --git a/IdentityShroud.Api/AppJsonSerializerContext.cs b/IdentityShroud.Api/AppJsonSerializerContext.cs new file mode 100644 index 0000000..9b075ce --- /dev/null +++ b/IdentityShroud.Api/AppJsonSerializerContext.cs @@ -0,0 +1,10 @@ +using System.Text.Json.Serialization; +using IdentityShroud.Core.Messages; +using IdentityShroud.Core.Messages.Realm; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +[JsonSerializable(typeof(OpenIdConfiguration))] +[JsonSerializable(typeof(RealmCreateRequest))] +internal partial class AppJsonSerializerContext : JsonSerializerContext +{ +} \ No newline at end of file diff --git a/IdentityShroud.Api/GlobalExceptionHandler.cs b/IdentityShroud.Api/GlobalExceptionHandler.cs deleted file mode 100644 index 7729674..0000000 --- a/IdentityShroud.Api/GlobalExceptionHandler.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Microsoft.AspNetCore.Diagnostics; - -namespace IdentityShroud.Api; - -public class GlobalExceptionHandler : IExceptionHandler -{ - private readonly ILogger _logger; - - public GlobalExceptionHandler(ILogger logger) - => _logger = logger; - - public async ValueTask TryHandleAsync( - HttpContext httpContext, - Exception exception, - CancellationToken cancellationToken) - { - _logger.LogError(exception, "Exception type: {Type}, Message: {Message}", - exception.GetType().Name, exception.Message); - - // Return false to let other handlers or the default handle it - // Return true to mark it as handled - return false; - } -} \ No newline at end of file diff --git a/IdentityShroud.Api/IdentityShroud.Api.csproj b/IdentityShroud.Api/IdentityShroud.Api.csproj index f6f4148..72b4639 100644 --- a/IdentityShroud.Api/IdentityShroud.Api.csproj +++ b/IdentityShroud.Api/IdentityShroud.Api.csproj @@ -5,7 +5,7 @@ enable enable true - false + true Linux 6b8ef434-0577-4a3c-8749-6b547d7787c5 @@ -15,17 +15,16 @@ - - - - - - + + + + + + - diff --git a/IdentityShroud.Api/Program.cs b/IdentityShroud.Api/Program.cs index 2ff5fe6..0a145c2 100644 --- a/IdentityShroud.Api/Program.cs +++ b/IdentityShroud.Api/Program.cs @@ -1,74 +1,70 @@ using FluentValidation; +using IdentityShroud.Api; using IdentityShroud.Api.Mappers; using IdentityShroud.Core; -using IdentityShroud.Core.EFCore; -using IdentityShroud.GraphQL; +using IdentityShroud.Core.Contracts; +using IdentityShroud.Core.Security; +using IdentityShroud.Core.Security.Keys; +using IdentityShroud.Core.Services; 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.AddScoped(); + services.AddSingleton(); + services.AddSingleton(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddOptions().Bind(configuration.GetSection("db")); + services.AddSingleton(); + services.AddScoped(); + + 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.MapApis(); + + // 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.Core.Tests/Fixtures/DbFixture.cs b/IdentityShroud.Core.Tests/Fixtures/DbFixture.cs index 1df6559..85c2fbe 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; 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/IdentityShroud.Core.Tests.csproj b/IdentityShroud.Core.Tests/IdentityShroud.Core.Tests.csproj index 918119c..40c87d5 100644 --- a/IdentityShroud.Core.Tests/IdentityShroud.Core.Tests.csproj +++ b/IdentityShroud.Core.Tests/IdentityShroud.Core.Tests.csproj @@ -1,4 +1,4 @@ - + net10.0 @@ -8,19 +8,20 @@ - - - - - - - - + + + + + + + + + - - + + @@ -29,4 +30,8 @@ + + + + \ No newline at end of file diff --git a/IdentityShroud.Core.Tests/JwtSignatureGeneratorTests.cs b/IdentityShroud.Core.Tests/JwtSignatureGeneratorTests.cs index 4563ea4..0fb0a42 100644 --- a/IdentityShroud.Core.Tests/JwtSignatureGeneratorTests.cs +++ b/IdentityShroud.Core.Tests/JwtSignatureGeneratorTests.cs @@ -48,7 +48,8 @@ public class JwtSignatureGeneratorTests } ] } - """; + """; + JsonWebKeySet keySet = JsonSerializer.Deserialize(keycloakKeySet)!; using RSA publicKey = LoadFromJwk(keySet.Keys[0]); @@ -71,8 +72,8 @@ public class JwtSignatureGeneratorTests var rsa = RSA.Create(); var parameters = new RSAParameters { - Modulus = WebEncoders.Base64UrlDecode(jwk.Modulus!), - Exponent = WebEncoders.Base64UrlDecode(jwk.Exponent!) + Modulus = WebEncoders.Base64UrlDecode(jwk.Modulus), + Exponent = WebEncoders.Base64UrlDecode(jwk.Exponent) }; rsa.ImportParameters(parameters); 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 index a0690c9..cb2e772 100644 --- a/IdentityShroud.Core.Tests/Services/ClientServiceTests.cs +++ b/IdentityShroud.Core.Tests/Services/ClientServiceTests.cs @@ -1,9 +1,5 @@ -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; @@ -11,34 +7,10 @@ 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 IEncryptionService _encryptionService = EncryptionServiceSubstitute.CreatePassthrough(); private readonly IClock _clock = Substitute.For(); private readonly Guid _realmId = new("a1b2c3d4-0000-0000-0000-000000000001"); @@ -61,28 +33,15 @@ public class ClientServiceTests : IClassFixture { if (!db.Realms.Any(r => r.Id == _realmId)) { - db.Realms.Add(new() - { - Id = _realmId, - Slug = "test-realm", - Name = "Test Realm", - DataEncryptionKeys = [ RealmDekBuilder.DefaultActive(), ], - }); - + db.Realms.Add(new() { Id = _realmId, Slug = "test-realm", Name = "Test Realm" }); db.SaveChanges(); } } - private ClientService CreateSut(Db db) => new(db, - _dataEncryptionService, - new ClientCreateRequestValidator(), - _clock); - - [Theory] [InlineData(false)] [InlineData(true)] - public async Task Create(bool withSecret) + public async Task Create(bool allowClientCredentialsFlow) { // Setup DateTime now = DateTime.UtcNow; @@ -92,13 +51,15 @@ public class ClientServiceTests : IClassFixture await using (var db = _dbFixture.CreateDbContext()) { // Act - ClientService sut = CreateSut(db); + ClientService sut = new(db, _encryptionService, _clock); var response = await sut.Create( _realmId, - ClientCreateRequestBuilder.Default() with + new ClientCreateRequest { - Confidential = withSecret, - GenerateSecret = withSecret, + ClientId = "test-client", + Name = "Test Client", + Description = "A test client", + AllowClientCredentialsFlow = allowClientCredentialsFlow, }, TestContext.Current.CancellationToken); @@ -108,7 +69,7 @@ public class ClientServiceTests : IClassFixture 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(allowClientCredentialsFlow, val.AllowClientCredentialsFlow); Assert.Equal(now, val.CreatedAt); } @@ -118,7 +79,7 @@ public class ClientServiceTests : IClassFixture .Include(e => e.Secrets) .SingleAsync(e => e.Id == val.Id, TestContext.Current.CancellationToken); - if (withSecret) + if (allowClientCredentialsFlow) Assert.Single(dbRecord.Secrets); else Assert.Empty(dbRecord.Secrets); @@ -146,8 +107,8 @@ public class ClientServiceTests : IClassFixture await using var actContext = _dbFixture.CreateDbContext(); // Act - ClientService sut = CreateSut(actContext); - Client? result = await sut.GetByClientId(_realmId, clientId, TestContext.Current.CancellationToken); + ClientService sut = new(actContext, _encryptionService, _clock); + Client? result = await sut.GetByClientId(clientId, TestContext.Current.CancellationToken); // Verify if (shouldFind) @@ -181,8 +142,8 @@ public class ClientServiceTests : IClassFixture await using var actContext = _dbFixture.CreateDbContext(); // Act - ClientService sut = CreateSut(actContext); - Client? result = await sut.FindById(_realmId, searchId, TestContext.Current.CancellationToken); + ClientService sut = new(actContext, _encryptionService, _clock); + Client? result = await sut.FindById(searchId, TestContext.Current.CancellationToken); // Verify if (shouldFind) 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..b855732 --- /dev/null +++ b/IdentityShroud.Core.Tests/Services/EncryptionServiceTests.cs @@ -0,0 +1,26 @@ +using System.Security.Cryptography; +using IdentityShroud.Core.Contracts; +using IdentityShroud.Core.Services; + +namespace IdentityShroud.Core.Tests.Services; + +public class EncryptionServiceTests +{ + [Fact] + public void RoundtripWorks() + { + // setup + string key = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32)); + var secretProvider = Substitute.For(); + secretProvider.GetSecret("Master").Returns(key); + + EncryptionService sut = new(secretProvider); + 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..60764bc 100644 --- a/IdentityShroud.Core.Tests/Services/RealmServiceTests.cs +++ b/IdentityShroud.Core.Tests/Services/RealmServiceTests.cs @@ -1,13 +1,10 @@ -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 Microsoft.EntityFrameworkCore; -using Shouldly; namespace IdentityShroud.Core.Tests.Services; @@ -15,7 +12,6 @@ public class RealmServiceTests : IClassFixture { private readonly DbFixture _dbFixture; private readonly IKeyService _keyService = Substitute.For(); - private readonly IDekEncryptionService _dekCryptor = new NullDekEncryptionService(); public RealmServiceTests(DbFixture dbFixture) { @@ -29,9 +25,6 @@ public class RealmServiceTests : IClassFixture { db.Database.ExecuteSqlRaw("TRUNCATE realm CASCADE;"); } - - private RealmService CreateSut(Db db) => new(db, _keyService, _dekCryptor, new ClockService()); - [Theory] [InlineData(null)] @@ -43,14 +36,14 @@ public class RealmServiceTests : IClassFixture if (idString is not null) realmId = new(idString); - Realm? val; + RealmCreateResponse? val; await using (var db = _dbFixture.CreateDbContext()) { _keyService.CreateKey(Arg.Any()) - .Returns(new CreateKeyResponse(KeyType.AES, new KeyData([21]))); + .Returns(new RealmKey(Guid.NewGuid(), "TST", [21], DateTime.UtcNow)); // Act - RealmService sut = CreateSut(db); - Result response = await sut.Create( + RealmService sut = new(db, _keyService); + var response = await sut.Create( new(realmId, "slug", "New realm"), TestContext.Current.CancellationToken); @@ -61,12 +54,8 @@ public class RealmServiceTests : IClassFixture 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) - ); + Assert.Equal("slug", val.Slug); + Assert.Equal("New realm", val.Name); _keyService.Received().CreateKey(Arg.Any()); } @@ -74,9 +63,9 @@ public class RealmServiceTests : IClassFixture await using (var db = _dbFixture.CreateDbContext()) { var dbRecord = await db.Realms - .Include(e => e.TokenSigningKeys) + .Include(e => e.Keys) .SingleAsync(e => e.Id == val.Id, TestContext.Current.CancellationToken); - Assert.Equal(KeyType.AES, dbRecord.TokenSigningKeys[0].KeyType); + Assert.Equal("TST", dbRecord.Keys[0].KeyType); } } @@ -103,7 +92,7 @@ public class RealmServiceTests : IClassFixture await using var actContext = _dbFixture.CreateDbContext(); // Act - RealmService sut = CreateSut(actContext); + RealmService sut = new(actContext, _keyService); var result = await sut.FindBySlug(slug, TestContext.Current.CancellationToken); // Verify @@ -136,7 +125,7 @@ public class RealmServiceTests : IClassFixture await using var actContext = _dbFixture.CreateDbContext(); // Act - RealmService sut = CreateSut(actContext); + RealmService sut = new(actContext, _keyService); Realm? result = await sut.FindById(id, TestContext.Current.CancellationToken); // Verify diff --git a/IdentityShroud.Core.Tests/UnitTest1.cs b/IdentityShroud.Core.Tests/UnitTest1.cs index 7cfc961..2d28047 100644 --- a/IdentityShroud.Core.Tests/UnitTest1.cs +++ b/IdentityShroud.Core.Tests/UnitTest1.cs @@ -1,7 +1,9 @@ -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 +36,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 +52,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 +67,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)) }; } } diff --git a/IdentityShroud.Core/Contracts/IClientService.cs b/IdentityShroud.Core/Contracts/IClientService.cs index 20e270c..15c0eba 100644 --- a/IdentityShroud.Core/Contracts/IClientService.cs +++ b/IdentityShroud.Core/Contracts/IClientService.cs @@ -2,6 +2,18 @@ using IdentityShroud.Core.Model; namespace IdentityShroud.Core.Contracts; +//public record CreateClientRequest(Guid RealmId, string ClientId, string? Description); + +public class ClientCreateRequest +{ + public required string ClientId { get; set; } + public string? Name { get; set; } + public string? Description { get; set; } + public string? SignatureAlgorithm { get; set; } + public bool? AllowClientCredentialsFlow { get; set; } +} + + public interface IClientService { Task> Create( @@ -9,6 +21,6 @@ public interface IClientService ClientCreateRequest request, CancellationToken ct = default); - Task GetByClientId(Guid realmId, string clientId, CancellationToken ct = default); - Task FindById(Guid realmId, int id, CancellationToken ct = default); + Task GetByClientId(string clientId, CancellationToken ct = default); + Task FindById(int id, CancellationToken ct = default); } \ 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..a737732 --- /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(ReadOnlyMemory cipher); +} \ No newline at end of file diff --git a/IdentityShroud.Core/Contracts/IKeyService.cs b/IdentityShroud.Core/Contracts/IKeyService.cs index 08a5bf6..4f6b5f7 100644 --- a/IdentityShroud.Core/Contracts/IKeyService.cs +++ b/IdentityShroud.Core/Contracts/IKeyService.cs @@ -1,10 +1,12 @@ +using IdentityShroud.Core.Messages; +using IdentityShroud.Core.Model; using IdentityShroud.Core.Security.Keys; namespace IdentityShroud.Core.Contracts; -public record CreateKeyResponse(KeyType KeyType, KeyData Key); - public interface IKeyService { - CreateKeyResponse CreateKey(KeyPolicy policy); + RealmKey CreateKey(KeyPolicy policy); + + JsonWebKey? CreateJsonWebKey(RealmKey realmKey); } \ 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 index 1724e6c..b740aa5 100644 --- a/IdentityShroud.Core/Contracts/IRealmService.cs +++ b/IdentityShroud.Core/Contracts/IRealmService.cs @@ -1,5 +1,6 @@ using IdentityShroud.Core.Messages.Realm; using IdentityShroud.Core.Model; +using IdentityShroud.Core.Services; namespace IdentityShroud.Core.Contracts; @@ -8,7 +9,6 @@ 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> 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..2a8e9e6 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); } 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..ea4d7d5 100644 --- a/IdentityShroud.Core/DTO/JsonWebKey.cs +++ b/IdentityShroud.Core/DTO/JsonWebKey.cs @@ -1,6 +1,7 @@ +using System.Buffers; +using System.Buffers.Text; +using System.Text.Json; using System.Text.Json.Serialization; -using IdentityShroud.Core.Helpers; -using IdentityShroud.Core.Security.Keys; namespace IdentityShroud.Core.Messages; @@ -10,7 +11,7 @@ namespace IdentityShroud.Core.Messages; 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")] @@ -47,4 +48,26 @@ public class JsonWebKey // [JsonPropertyName("x5t")] // [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] // public string? X509CertificateThumbprint { get; set; } +} + +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/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 54% rename from IdentityShroud.Core/EFCore/Db.cs rename to IdentityShroud.Core/Db.cs index b2bc12e..cd7a493 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 { @@ -20,9 +18,8 @@ public class Db( { public virtual DbSet Clients { get; set; } public virtual DbSet Realms { get; set; } - public virtual DbSet Keys { get; set; } - public virtual DbSet Deks { get; set; } - + public virtual DbSet Keys { get; set; } + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseNpgsql(""); @@ -38,22 +35,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..beef894 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; diff --git a/IdentityShroud.Core/IdentityShroud.Core.csproj b/IdentityShroud.Core/IdentityShroud.Core.csproj index fe5ed22..d9d6809 100644 --- a/IdentityShroud.Core/IdentityShroud.Core.csproj +++ b/IdentityShroud.Core/IdentityShroud.Core.csproj @@ -1,4 +1,4 @@ - + net10.0 @@ -7,24 +7,23 @@ - - - - - - - - - - + + + + + + + - + - + + ..\..\..\.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..a8c9e29 100644 --- a/IdentityShroud.Core/Model/Client.cs +++ b/IdentityShroud.Core/Model/Client.cs @@ -1,5 +1,6 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using IdentityShroud.Core.Security; using Microsoft.EntityFrameworkCore; namespace IdentityShroud.Core.Model; @@ -19,16 +20,8 @@ public class Client public string? Description { get; set; } [MaxLength(20)] - public JwtSigAlgName? SignatureAlgorithm { get; set; } + public string? 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; } diff --git a/IdentityShroud.Core/Model/ClientSecret.cs b/IdentityShroud.Core/Model/ClientSecret.cs index 189039f..bd57d37 100644 --- a/IdentityShroud.Core/Model/ClientSecret.cs +++ b/IdentityShroud.Core/Model/ClientSecret.cs @@ -1,8 +1,5 @@ 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; @@ -13,17 +10,6 @@ public class ClientSecret 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); - } + public required byte[] SecretEncrypted { get; set; } } \ 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..c02fc38 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.Security; +using Microsoft.EntityFrameworkCore; namespace IdentityShroud.Core.Model; [Table("realm")] public class Realm { + 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,11 @@ 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 List DataEncryptionKeys { get; init; } = []; + public List Keys { get; init; } = []; /// /// Can be overriden per client /// - public JwtSigAlgName DefaultSignatureAlgorithm { get; set; } = JwtSigAlgName.RS256; -} \ No newline at end of file + public string DefaultSignatureAlgorithm { get; set; } = JsonWebAlgorithm.RS256; + +} 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/RealmKey.cs b/IdentityShroud.Core/Model/RealmKey.cs new file mode 100644 index 0000000..14c7c9c --- /dev/null +++ b/IdentityShroud.Core/Model/RealmKey.cs @@ -0,0 +1,22 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace IdentityShroud.Core.Model; + + +[Table("realm_key")] +public record RealmKey(Guid Id, string KeyType, byte[] KeyDataEncrypted, DateTime CreatedAt) +{ + public Guid Id { get; private set; } = Id; + public string KeyType { get; private set; } = KeyType; + public byte[] KeyDataEncrypted { get; private set; } = KeyDataEncrypted; + public DateTime CreatedAt { get; private set; } = CreatedAt; + 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; + + +} \ No newline at end of file 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..bfa5809 --- /dev/null +++ b/IdentityShroud.Core/Security/AesGcmHelper.cs @@ -0,0 +1,70 @@ +using System.Security.Cryptography; + +namespace IdentityShroud.Core.Security; + +public static class AesGcmHelper +{ + + public static byte[] EncryptAesGcm(byte[] plaintext, byte[] key) + { + int tagSize = AesGcm.TagByteSizes.MaxSize; + using var aes = new AesGcm(key, tagSize); + + Span nonce = stackalloc byte[AesGcm.NonceByteSizes.MaxSize]; + RandomNumberGenerator.Fill(nonce); + Span ciphertext = stackalloc byte[plaintext.Length]; + Span tag = stackalloc byte[tagSize]; + + aes.Encrypt(nonce, plaintext, ciphertext, tag); + + // Return concatenated nonce|ciphertext|tag + var result = new byte[nonce.Length + ciphertext.Length + tag.Length]; + nonce.CopyTo(result.AsSpan(0, nonce.Length)); + ciphertext.CopyTo(result.AsSpan(nonce.Length, ciphertext.Length)); + tag.CopyTo(result.AsSpan(nonce.Length + ciphertext.Length, tag.Length)); + return result; + } + + // -------------------------------------------------------------------- + // 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(ReadOnlyMemory payload, byte[] key) + { + 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 = payload.Span[..nonceSize]; + ReadOnlySpan ciphertext = payload.Span.Slice(nonceSize, payload.Length - nonceSize - tagSize); + ReadOnlySpan tag = payload.Span.Slice(payload.Length - tagSize, tagSize); + + byte[] plaintext = new byte[ciphertext.Length]; + + using var aes = new AesGcm(key, tagSize); + 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..ab77ef1 100644 --- a/IdentityShroud.Core/Security/ConfigurationSecretProvider.cs +++ b/IdentityShroud.Core/Security/ConfigurationSecretProvider.cs @@ -14,9 +14,4 @@ public class ConfigurationSecretProvider(IConfiguration configuration) : ISecret { 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/JsonWebAlgorithm.cs b/IdentityShroud.Core/Security/JsonWebAlgorithm.cs new file mode 100644 index 0000000..cbdcf05 --- /dev/null +++ b/IdentityShroud.Core/Security/JsonWebAlgorithm.cs @@ -0,0 +1,8 @@ +using System.Security.Cryptography; + +namespace IdentityShroud.Core.Security; + +public static class JsonWebAlgorithm +{ + public const string RS256 = "RS256"; +} \ 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..e22cfca --- /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 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}"; + } +} \ 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 index 6a5ce45..ec095b5 100644 --- a/IdentityShroud.Core/Security/Keys/IKeyProvider.cs +++ b/IdentityShroud.Core/Security/Keys/IKeyProvider.cs @@ -1,33 +1,19 @@ using IdentityShroud.Core.Messages; +using IdentityShroud.Core.Model; namespace IdentityShroud.Core.Security.Keys; -public class KeyPolicy +public abstract 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 abstract string KeyType { get; } } public interface IKeyProvider { - /// - /// Returns true when this key uses public key cryptography - /// - bool IsPublic { get; } - KeyData CreateKey(KeyPolicy policy); + byte[] CreateKey(KeyPolicy policy); - void SetJwkParameters(Dictionary parameters, JsonWebKey jwk); + void SetJwkParameters(byte[] key, JsonWebKey jwk); } diff --git a/IdentityShroud.Core/Security/Keys/IKeyProviderFactory.cs b/IdentityShroud.Core/Security/Keys/IKeyProviderFactory.cs index c39a836..485e6e5 100644 --- a/IdentityShroud.Core/Security/Keys/IKeyProviderFactory.cs +++ b/IdentityShroud.Core/Security/Keys/IKeyProviderFactory.cs @@ -3,5 +3,5 @@ namespace IdentityShroud.Core.Security.Keys; public interface IKeyProviderFactory { - public IKeyProvider CreateProvider(KeyType keyType); + public IKeyProvider CreateProvider(string keyType); } \ No newline at end of file diff --git a/IdentityShroud.Core/Security/Keys/KeyProviderFactory.cs b/IdentityShroud.Core/Security/Keys/KeyProviderFactory.cs index 33d5092..a1c3472 100644 --- a/IdentityShroud.Core/Security/Keys/KeyProviderFactory.cs +++ b/IdentityShroud.Core/Security/Keys/KeyProviderFactory.cs @@ -1,18 +1,15 @@ -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) + public IKeyProvider CreateProvider(string keyType) { - switch (keyType.Name) + switch (keyType) { case "RSA": return new RsaProvider(); - case "AES": - return new AesProvider(); default: throw new NotImplementedException(); } 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 index 717f9de..a5bcee8 100644 --- a/IdentityShroud.Core/Security/Keys/Rsa/RsaProvider.cs +++ b/IdentityShroud.Core/Security/Keys/Rsa/RsaProvider.cs @@ -1,34 +1,37 @@ using System.Buffers.Text; using System.Security.Cryptography; +using IdentityShroud.Core.Contracts; using IdentityShroud.Core.Messages; +using IdentityShroud.Core.Model; namespace IdentityShroud.Core.Security.Keys.Rsa; +public class RsaKeyPolicy : KeyPolicy +{ + public override string KeyType => "RSA"; + public int KeySize { get; } = 2048; +} + public class RsaProvider : IKeyProvider { - public bool IsPublic => true; - - public KeyData CreateKey(KeyPolicy policy) + public byte[] 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), - }); + return rsa.ExportPkcs8PrivateKey(); } throw new ArgumentException("Incorrect policy type", nameof(policy)); } - public void SetJwkParameters(Dictionary parameters, JsonWebKey jwk) + public void SetJwkParameters(byte[] key, JsonWebKey jwk) { - jwk.Exponent = parameters["e"]; - jwk.Modulus = parameters["n"]; + using var rsa = RSA.Create(); + rsa.ImportPkcs8PrivateKey(key, out _); + var parameters = rsa.ExportParameters(includePrivateParameters: false); + + jwk.Exponent = Base64Url.EncodeToString(parameters.Exponent); + jwk.Modulus = Base64Url.EncodeToString(parameters.Modulus); } } \ 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..ab49ebd --- /dev/null +++ b/IdentityShroud.Core/Security/RsaHelper.cs @@ -0,0 +1,16 @@ +using System.Security.Cryptography; + +namespace IdentityShroud.Core.Security; + +public static class RsaHelper +{ + /// + /// 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/Services/ClientService.cs b/IdentityShroud.Core/Services/ClientService.cs index 61be016..2e556d4 100644 --- a/IdentityShroud.Core/Services/ClientService.cs +++ b/IdentityShroud.Core/Services/ClientService.cs @@ -1,7 +1,5 @@ using System.Security.Cryptography; -using FluentValidation; using IdentityShroud.Core.Contracts; -using IdentityShroud.Core.EFCore; using IdentityShroud.Core.Model; using Microsoft.EntityFrameworkCore; @@ -9,36 +7,25 @@ namespace IdentityShroud.Core.Services; public class ClientService( Db db, - IDataEncryptionService cryptor, - IValidator clientCreateValidator, + IEncryptionService cryptor, 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, + SignatureAlgorithm = request.SignatureAlgorithm, + AllowClientCredentialsFlow = request.AllowClientCredentialsFlow ?? false, CreatedAt = clock.UtcNow(), }; - if (request.GenerateSecret is true) + if (client.AllowClientCredentialsFlow) { - await db.Entry(realm).Collection(r => r.DataEncryptionKeys) - .Query() - .LoadAsync(ct); - - client.Secrets.Add(CreateSecret(realm)); + client.Secrets.Add(CreateSecret()); } await db.AddAsync(client, ct); @@ -47,33 +34,24 @@ public class ClientService( return client; } - public async Task GetByClientId( - Guid realmId, - string clientId, - CancellationToken ct = default) + public async Task GetByClientId(string clientId, CancellationToken ct = default) { - return await db.Clients.FirstOrDefaultAsync(c => c.ClientId == clientId && c.RealmId == realmId, ct); + return await db.Clients.FirstOrDefaultAsync(c => c.ClientId == clientId, ct); } - public async Task FindById( - Guid realmId, - int id, - CancellationToken ct = default) + public async Task FindById(int id, CancellationToken ct = default) { - return await db.Clients.FirstOrDefaultAsync(c => c.Id == id && c.RealmId == realmId, ct); + return await db.Clients.FirstOrDefaultAsync(c => c.Id == id, ct); } - private ClientSecret CreateSecret(Realm realm) + private ClientSecret CreateSecret() { - Span secret = stackalloc byte[24]; - RandomNumberGenerator.Fill(secret); - - var dek = realm.DataEncryptionKeys.Single(k => k.Active); + byte[] secret = RandomNumberGenerator.GetBytes(24); return new ClientSecret() { CreatedAt = clock.UtcNow(), - Secret = cryptor.Encrypt(dek, secret), + SecretEncrypted = cryptor.Encrypt(secret), }; } 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/EncryptionService.cs b/IdentityShroud.Core/Services/EncryptionService.cs new file mode 100644 index 0000000..a4455e0 --- /dev/null +++ b/IdentityShroud.Core/Services/EncryptionService.cs @@ -0,0 +1,27 @@ +using IdentityShroud.Core.Contracts; +using IdentityShroud.Core.Security; + +namespace IdentityShroud.Core.Services; + +/// +/// +/// +public class EncryptionService : IEncryptionService +{ + private readonly byte[] encryptionKey; + + public EncryptionService(ISecretProvider secretProvider) + { + encryptionKey = Convert.FromBase64String(secretProvider.GetSecret("Master")); + } + + public byte[] Encrypt(byte[] plain) + { + return AesGcmHelper.EncryptAesGcm(plain, encryptionKey); + } + + public byte[] Decrypt(ReadOnlyMemory cipher) + { + return AesGcmHelper.DecryptAesGcm(cipher, encryptionKey); + } +} \ No newline at end of file diff --git a/IdentityShroud.Core/Services/KeyService.cs b/IdentityShroud.Core/Services/KeyService.cs index 10900dd..440dff9 100644 --- a/IdentityShroud.Core/Services/KeyService.cs +++ b/IdentityShroud.Core/Services/KeyService.cs @@ -1,16 +1,52 @@ +using System.Security.Cryptography; using IdentityShroud.Core.Contracts; +using IdentityShroud.Core.Messages; +using IdentityShroud.Core.Model; using IdentityShroud.Core.Security.Keys; namespace IdentityShroud.Core.Services; public class KeyService( - IKeyProviderFactory keyProviderFactory) : IKeyService + IEncryptionService cryptor, + IKeyProviderFactory keyProviderFactory, + IClock clock) : IKeyService { - public CreateKeyResponse CreateKey(KeyPolicy policy) + public RealmKey CreateKey(KeyPolicy policy) { IKeyProvider provider = keyProviderFactory.CreateProvider(policy.KeyType); - KeyData plainKey = provider.CreateKey(policy); + var plainKey = provider.CreateKey(policy); - return new CreateKeyResponse(policy.KeyType, plainKey); + return CreateKey(policy.KeyType, plainKey); } + + public JsonWebKey? CreateJsonWebKey(RealmKey realmKey) + { + JsonWebKey jwk = new() + { + KeyId = realmKey.Id.ToString(), + KeyType = realmKey.KeyType, + Use = "sig", + }; + + IKeyProvider provider = keyProviderFactory.CreateProvider(realmKey.KeyType); + provider.SetJwkParameters( + cryptor.Decrypt(realmKey.KeyDataEncrypted), + jwk); + + return jwk; + } + + private RealmKey CreateKey(string keyType, byte[] plainKey) => + new RealmKey( + Guid.NewGuid(), + keyType, + cryptor.Encrypt(plainKey), + clock.UtcNow()); + + // public byte[] GetPrivateKey(IEncryptionService encryptionService) + // { + // if (_privateKeyDecrypted.Length == 0 && PrivateKeyEncrypted.Length > 0) + // _privateKeyDecrypted = encryptionService.Decrypt(PrivateKeyEncrypted); + // return _privateKeyDecrypted; + // } } 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..5385658 100644 --- a/IdentityShroud.Core/Services/RealmService.cs +++ b/IdentityShroud.Core/Services/RealmService.cs @@ -1,21 +1,19 @@ +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 + IKeyService keyService) : IRealmService { public async Task FindById(Guid id, CancellationToken ct = default) { @@ -29,7 +27,7 @@ public class RealmService( .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 +36,29 @@ public class RealmService( Name = request.Name, }; - realm.TokenSigningKeys.Add(CreateSigningKey(realm)); - realm.DataEncryptionKeys.Add(CreateDataEncryptionKey(realm)); + realm.Keys.Add(keyService.CreateKey(GetKeyPolicy(realm))); db.Add(realm); await db.SaveChangesAsync(ct); - - return realm; + + return new RealmCreateResponse( + realm.Id, realm.Slug, realm.Name); } - - 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(); + private KeyPolicy GetKeyPolicy(Realm _) => new RsaKeyPolicy(); + public async Task LoadActiveKeys(Realm realm) { - await db.Entry(realm).Collection(r => r.TokenSigningKeys) + await db.Entry(realm).Collection(r => r.Keys) .Query() .Where(k => k.RevokedAt == null) .LoadAsync(); - } - - public async Task LoadDeks(Realm realm) - { - await db.Entry(realm).Collection(r => r.DataEncryptionKeys) - .Query() - .LoadAsync(); + } } \ 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("Slug") - .IsRequired() - .HasMaxLength(40) - .HasColumnType("character varying(40)") - .HasColumnName("slug"); - - b.HasKey("Id") - .HasName("pk_realm"); - - b.ToTable("realm", (string)null); - }); - - modelBuilder.Entity("IdentityShroud.Core.Model.RealmDek", b => - { - b.Property("Id") - .HasColumnType("uuid") - .HasColumnName("id"); - - b.Property("Active") - .HasColumnType("boolean") - .HasColumnName("active"); - - b.Property("Algorithm") - .IsRequired() - .HasColumnType("text") - .HasColumnName("algorithm"); - - b.Property("RealmId") - .HasColumnType("uuid") - .HasColumnName("realm_id"); - - b.ComplexProperty(typeof(Dictionary), "KeyData", "IdentityShroud.Core.Model.RealmDek.KeyData#EncryptedDek", b1 => - { - b1.IsRequired(); - - b1.Property("KekId") - .HasColumnType("uuid") - .HasColumnName("key_data_kek_id"); - - b1.Property("Value") - .IsRequired() - .HasColumnType("bytea") - .HasColumnName("key_data_value"); - }); - - b.HasKey("Id") - .HasName("pk_realm_dek"); - - b.HasIndex("RealmId") - .HasDatabaseName("ix_realm_dek_realm_id"); - - b.ToTable("realm_dek", (string)null); - }); - - modelBuilder.Entity("IdentityShroud.Core.Model.RealmSigningKey", b => - { - b.Property("Id") - .HasColumnType("uuid") - .HasColumnName("id"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("created_at"); - - b.Property("KeyType") - .IsRequired() - .HasColumnType("text") - .HasColumnName("key_type"); - - b.Property("Priority") - .HasColumnType("integer") - .HasColumnName("priority"); - - b.Property("PublicKeyParameters") - .HasColumnType("jsonb") - .HasColumnName("public_key_parameters"); - - b.Property("RealmId") - .HasColumnType("uuid") - .HasColumnName("realm_id"); - - b.Property("RevokedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("revoked_at"); - - b.ComplexProperty(typeof(Dictionary), "Key", "IdentityShroud.Core.Model.RealmSigningKey.Key#EncryptedDek", b1 => - { - b1.IsRequired(); - - b1.Property("KekId") - .HasColumnType("uuid") - .HasColumnName("key_kek_id"); - - b1.Property("Value") - .IsRequired() - .HasColumnType("bytea") - .HasColumnName("key_value"); - }); - - b.HasKey("Id") - .HasName("pk_realm_key"); - - b.HasIndex("RealmId") - .HasDatabaseName("ix_realm_key_realm_id"); - - b.ToTable("realm_key", (string)null); - }); - - modelBuilder.Entity("IdentityShroud.Core.Model.Client", b => - { - b.HasOne("IdentityShroud.Core.Model.Realm", null) - .WithMany("Clients") - .HasForeignKey("RealmId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_client_realm_realm_id"); - }); - - modelBuilder.Entity("IdentityShroud.Core.Model.ClientSecret", b => - { - b.HasOne("IdentityShroud.Core.Model.Client", null) - .WithMany("Secrets") - .HasForeignKey("ClientId1") - .HasConstraintName("fk_client_secret_client_client_id1"); - }); - - modelBuilder.Entity("IdentityShroud.Core.Model.RealmDek", b => - { - b.HasOne("IdentityShroud.Core.Model.Realm", null) - .WithMany("DataEncryptionKeys") - .HasForeignKey("RealmId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_realm_dek_realm_realm_id"); - }); - - modelBuilder.Entity("IdentityShroud.Core.Model.RealmSigningKey", b => - { - b.HasOne("IdentityShroud.Core.Model.Realm", null) - .WithMany("TokenSigningKeys") - .HasForeignKey("RealmId") - .HasConstraintName("fk_realm_key_realm_realm_id"); - }); - - modelBuilder.Entity("IdentityShroud.Core.Model.Client", b => - { - b.Navigation("Secrets"); - }); - - modelBuilder.Entity("IdentityShroud.Core.Model.Realm", b => - { - b.Navigation("Clients"); - - b.Navigation("DataEncryptionKeys"); - - b.Navigation("TokenSigningKeys"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/IdentityShroud.Migrations/Migrations/20260412083710_Initial.cs b/IdentityShroud.Migrations/Migrations/20260412083710_Initial.cs deleted file mode 100644 index 78401bb..0000000 --- a/IdentityShroud.Migrations/Migrations/20260412083710_Initial.cs +++ /dev/null @@ -1,171 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace IdentityShroud.Migrations.Migrations -{ - /// - public partial class Initial : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "realm", - columns: table => new - { - id = table.Column(type: "uuid", nullable: false), - slug = table.Column(type: "character varying(40)", maxLength: 40, nullable: false), - name = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), - default_signature_algorithm = table.Column(type: "text", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_realm", x => x.id); - }); - - migrationBuilder.CreateTable( - name: "client", - columns: table => new - { - id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - realm_id = table.Column(type: "uuid", nullable: false), - client_id = table.Column(type: "character varying(40)", maxLength: 40, nullable: false), - name = table.Column(type: "character varying(80)", maxLength: 80, nullable: true), - description = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: true), - signature_algorithm = table.Column(type: "character varying(20)", maxLength: 20, nullable: true), - confidential = table.Column(type: "boolean", nullable: false), - allow_client_credentials_flow = table.Column(type: "boolean", nullable: false), - created_at = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_client", x => x.id); - table.ForeignKey( - name: "fk_client_realm_realm_id", - column: x => x.realm_id, - principalTable: "realm", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "realm_dek", - columns: table => new - { - id = table.Column(type: "uuid", nullable: false), - active = table.Column(type: "boolean", nullable: false), - algorithm = table.Column(type: "text", nullable: false), - realm_id = table.Column(type: "uuid", nullable: false), - key_data_kek_id = table.Column(type: "uuid", nullable: false), - key_data_value = table.Column(type: "bytea", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_realm_dek", x => x.id); - table.ForeignKey( - name: "fk_realm_dek_realm_realm_id", - column: x => x.realm_id, - principalTable: "realm", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "realm_key", - columns: table => new - { - id = table.Column(type: "uuid", nullable: false), - key_type = table.Column(type: "text", nullable: false), - created_at = table.Column(type: "timestamp with time zone", nullable: false), - revoked_at = table.Column(type: "timestamp with time zone", nullable: true), - priority = table.Column(type: "integer", nullable: false), - public_key_parameters = table.Column(type: "jsonb", nullable: true), - realm_id = table.Column(type: "uuid", nullable: true), - key_kek_id = table.Column(type: "uuid", nullable: false), - key_value = table.Column(type: "bytea", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_realm_key", x => x.id); - table.ForeignKey( - name: "fk_realm_key_realm_realm_id", - column: x => x.realm_id, - principalTable: "realm", - principalColumn: "id"); - }); - - migrationBuilder.CreateTable( - name: "client_secret", - columns: table => new - { - id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - client_id = table.Column(type: "uuid", nullable: false), - created_at = table.Column(type: "timestamp with time zone", nullable: false), - expires = table.Column(type: "timestamp with time zone", nullable: true), - revoked_at = table.Column(type: "timestamp with time zone", nullable: true), - client_id1 = table.Column(type: "integer", nullable: true), - secret_dek_id = table.Column(type: "uuid", nullable: false), - secret_value = table.Column(type: "bytea", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_client_secret", x => x.id); - table.ForeignKey( - name: "fk_client_secret_client_client_id1", - column: x => x.client_id1, - principalTable: "client", - principalColumn: "id"); - }); - - migrationBuilder.CreateIndex( - name: "ix_client_client_id", - table: "client", - column: "client_id", - unique: true); - - migrationBuilder.CreateIndex( - name: "ix_client_realm_id", - table: "client", - column: "realm_id"); - - migrationBuilder.CreateIndex( - name: "ix_client_secret_client_id1", - table: "client_secret", - column: "client_id1"); - - migrationBuilder.CreateIndex( - name: "ix_realm_dek_realm_id", - table: "realm_dek", - column: "realm_id"); - - migrationBuilder.CreateIndex( - name: "ix_realm_key_realm_id", - table: "realm_key", - column: "realm_id"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "client_secret"); - - migrationBuilder.DropTable( - name: "realm_dek"); - - migrationBuilder.DropTable( - name: "realm_key"); - - migrationBuilder.DropTable( - name: "client"); - - migrationBuilder.DropTable( - name: "realm"); - } - } -} diff --git a/IdentityShroud.Migrations/Migrations/DbModelSnapshot.cs b/IdentityShroud.Migrations/Migrations/DbModelSnapshot.cs deleted file mode 100644 index f16ec76..0000000 --- a/IdentityShroud.Migrations/Migrations/DbModelSnapshot.cs +++ /dev/null @@ -1,315 +0,0 @@ -// -using System; -using System.Collections.Generic; -using IdentityShroud.Core.EFCore; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace IdentityShroud.Migrations.Migrations -{ - [DbContext(typeof(Db))] - partial class DbModelSnapshot : ModelSnapshot - { - protected override void BuildModel(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("Slug") - .IsRequired() - .HasMaxLength(40) - .HasColumnType("character varying(40)") - .HasColumnName("slug"); - - b.HasKey("Id") - .HasName("pk_realm"); - - b.ToTable("realm", (string)null); - }); - - modelBuilder.Entity("IdentityShroud.Core.Model.RealmDek", b => - { - b.Property("Id") - .HasColumnType("uuid") - .HasColumnName("id"); - - b.Property("Active") - .HasColumnType("boolean") - .HasColumnName("active"); - - b.Property("Algorithm") - .IsRequired() - .HasColumnType("text") - .HasColumnName("algorithm"); - - b.Property("RealmId") - .HasColumnType("uuid") - .HasColumnName("realm_id"); - - b.ComplexProperty(typeof(Dictionary), "KeyData", "IdentityShroud.Core.Model.RealmDek.KeyData#EncryptedDek", b1 => - { - b1.IsRequired(); - - b1.Property("KekId") - .HasColumnType("uuid") - .HasColumnName("key_data_kek_id"); - - b1.Property("Value") - .IsRequired() - .HasColumnType("bytea") - .HasColumnName("key_data_value"); - }); - - b.HasKey("Id") - .HasName("pk_realm_dek"); - - b.HasIndex("RealmId") - .HasDatabaseName("ix_realm_dek_realm_id"); - - b.ToTable("realm_dek", (string)null); - }); - - modelBuilder.Entity("IdentityShroud.Core.Model.RealmSigningKey", b => - { - b.Property("Id") - .HasColumnType("uuid") - .HasColumnName("id"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("created_at"); - - b.Property("KeyType") - .IsRequired() - .HasColumnType("text") - .HasColumnName("key_type"); - - b.Property("Priority") - .HasColumnType("integer") - .HasColumnName("priority"); - - b.Property("PublicKeyParameters") - .HasColumnType("jsonb") - .HasColumnName("public_key_parameters"); - - b.Property("RealmId") - .HasColumnType("uuid") - .HasColumnName("realm_id"); - - b.Property("RevokedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("revoked_at"); - - b.ComplexProperty(typeof(Dictionary), "Key", "IdentityShroud.Core.Model.RealmSigningKey.Key#EncryptedDek", b1 => - { - b1.IsRequired(); - - b1.Property("KekId") - .HasColumnType("uuid") - .HasColumnName("key_kek_id"); - - b1.Property("Value") - .IsRequired() - .HasColumnType("bytea") - .HasColumnName("key_value"); - }); - - b.HasKey("Id") - .HasName("pk_realm_key"); - - b.HasIndex("RealmId") - .HasDatabaseName("ix_realm_key_realm_id"); - - b.ToTable("realm_key", (string)null); - }); - - modelBuilder.Entity("IdentityShroud.Core.Model.Client", b => - { - b.HasOne("IdentityShroud.Core.Model.Realm", null) - .WithMany("Clients") - .HasForeignKey("RealmId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_client_realm_realm_id"); - }); - - modelBuilder.Entity("IdentityShroud.Core.Model.ClientSecret", b => - { - b.HasOne("IdentityShroud.Core.Model.Client", null) - .WithMany("Secrets") - .HasForeignKey("ClientId1") - .HasConstraintName("fk_client_secret_client_client_id1"); - }); - - modelBuilder.Entity("IdentityShroud.Core.Model.RealmDek", b => - { - b.HasOne("IdentityShroud.Core.Model.Realm", null) - .WithMany("DataEncryptionKeys") - .HasForeignKey("RealmId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_realm_dek_realm_realm_id"); - }); - - modelBuilder.Entity("IdentityShroud.Core.Model.RealmSigningKey", b => - { - b.HasOne("IdentityShroud.Core.Model.Realm", null) - .WithMany("TokenSigningKeys") - .HasForeignKey("RealmId") - .HasConstraintName("fk_realm_key_realm_realm_id"); - }); - - modelBuilder.Entity("IdentityShroud.Core.Model.Client", b => - { - b.Navigation("Secrets"); - }); - - modelBuilder.Entity("IdentityShroud.Core.Model.Realm", b => - { - b.Navigation("Clients"); - - b.Navigation("DataEncryptionKeys"); - - b.Navigation("TokenSigningKeys"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/IdentityShroud.PluginSupport/IPlugin.cs b/IdentityShroud.PluginSupport/IPlugin.cs deleted file mode 100644 index fcae4fc..0000000 --- a/IdentityShroud.PluginSupport/IPlugin.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace IdentityShroud.PluginSupport; - -/// -/// Any class that should be discovered when loading a dll should implement IPlugin -/// -public interface IPlugin -{ - -} \ No newline at end of file diff --git a/IdentityShroud.PluginSupport/IdentityShroud.PluginSupport.csproj b/IdentityShroud.PluginSupport/IdentityShroud.PluginSupport.csproj deleted file mode 100644 index 237d661..0000000 --- a/IdentityShroud.PluginSupport/IdentityShroud.PluginSupport.csproj +++ /dev/null @@ -1,9 +0,0 @@ - - - - net10.0 - enable - enable - - - diff --git a/IdentityShroud.SecretProviders/ISecretProvider.cs b/IdentityShroud.SecretProviders/ISecretProvider.cs deleted file mode 100644 index 666dd1e..0000000 --- a/IdentityShroud.SecretProviders/ISecretProvider.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace IdentityShroud.SecretProviders; - -/// -/// Required interface of a SecretProvider. -/// -public interface ISecretProvider -{ - /// - /// Used as a key in the registry. This same value should be used for the type field - /// when configuring a secret. - /// - string Key { get; } - - Task GetSecret(string configurationValue, CancellationToken ct = default); -} \ No newline at end of file diff --git a/IdentityShroud.SecretProviders/IdentityShroud.SecretProviders.csproj b/IdentityShroud.SecretProviders/IdentityShroud.SecretProviders.csproj deleted file mode 100644 index 3de771e..0000000 --- a/IdentityShroud.SecretProviders/IdentityShroud.SecretProviders.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - net10.0 - enable - enable - - - - - - - diff --git a/IdentityShroud.SecretProviders/PlainSecret.cs b/IdentityShroud.SecretProviders/PlainSecret.cs deleted file mode 100644 index 9aca02a..0000000 --- a/IdentityShroud.SecretProviders/PlainSecret.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Security.Cryptography; - -namespace IdentityShroud.SecretProviders; - -public sealed class PlainSecret(byte[] secret) : IDisposable -{ - private bool _disposed; - - public ReadOnlySpan Secret => secret; - - public void Dispose() - { - if (_disposed) - return; - _disposed = true; - CryptographicOperations.ZeroMemory(secret); - } -} \ No newline at end of file diff --git a/IdentityShroud.TestUtils.Tests/IdentityShroud.TestUtils.Tests.csproj b/IdentityShroud.TestUtils.Tests/IdentityShroud.TestUtils.Tests.csproj index 8e5e7f7..9ce8074 100644 --- a/IdentityShroud.TestUtils.Tests/IdentityShroud.TestUtils.Tests.csproj +++ b/IdentityShroud.TestUtils.Tests/IdentityShroud.TestUtils.Tests.csproj @@ -7,16 +7,16 @@ - - - - - + + + + + - - + + diff --git a/IdentityShroud.TestUtils/Asserts/JsonObjectAssert.cs b/IdentityShroud.TestUtils/Asserts/JsonObjectAssert.cs index 9dbf957..3352bc6 100644 --- a/IdentityShroud.TestUtils/Asserts/JsonObjectAssert.cs +++ b/IdentityShroud.TestUtils/Asserts/JsonObjectAssert.cs @@ -1,5 +1,6 @@ using System.Text.Json.Nodes; using System.Text.RegularExpressions; +using Xunit; namespace IdentityShroud.TestUtils.Asserts; @@ -35,8 +36,6 @@ public static class JsonObjectAssert return segments.ToArray(); } - public static JsonNode? NavigateToPath(JsonObject jsonObject, string path) - => NavigateToPath(jsonObject, ParsePath(path)); /// /// Navigates to a JsonNode at the specified path and returns it. /// Throws XunitException if the path doesn't exist or is invalid. diff --git a/IdentityShroud.TestUtils/Asserts/ResultAssert.cs b/IdentityShroud.TestUtils/Asserts/ResultAssert.cs index ff00c06..28a0b11 100644 --- a/IdentityShroud.TestUtils/Asserts/ResultAssert.cs +++ b/IdentityShroud.TestUtils/Asserts/ResultAssert.cs @@ -1,4 +1,5 @@ using FluentResults; +using Xunit; namespace IdentityShroud.Core.Tests; diff --git a/IdentityShroud.TestUtils/IdentityShroud.TestUtils.csproj b/IdentityShroud.TestUtils/IdentityShroud.TestUtils.csproj index db517cc..0b8cba9 100644 --- a/IdentityShroud.TestUtils/IdentityShroud.TestUtils.csproj +++ b/IdentityShroud.TestUtils/IdentityShroud.TestUtils.csproj @@ -8,9 +8,8 @@ - - - + + @@ -18,8 +17,14 @@ - - + + + + + + + ..\..\..\.nuget\packages\nsubstitute\5.3.0\lib\net6.0\NSubstitute.dll + diff --git a/IdentityShroud.TestUtils/Substitutes/EncryptionServiceSubstitute.cs b/IdentityShroud.TestUtils/Substitutes/EncryptionServiceSubstitute.cs new file mode 100644 index 0000000..5a81240 --- /dev/null +++ b/IdentityShroud.TestUtils/Substitutes/EncryptionServiceSubstitute.cs @@ -0,0 +1,18 @@ +using IdentityShroud.Core.Contracts; + +namespace IdentityShroud.TestUtils.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).ToArray()); + return encryptionService; + } +} \ No newline at end of file diff --git a/IdentityShroud.TestUtils/Substitutes/NullDataEncryptionService.cs b/IdentityShroud.TestUtils/Substitutes/NullDataEncryptionService.cs deleted file mode 100644 index eaf1180..0000000 --- a/IdentityShroud.TestUtils/Substitutes/NullDataEncryptionService.cs +++ /dev/null @@ -1,19 +0,0 @@ -using IdentityShroud.Core.Contracts; -using IdentityShroud.Core.Model; -using IdentityShroud.Core.Security; - -namespace IdentityShroud.TestUtils.Substitutes; - -public class NullDataEncryptionService : IDataEncryptionService -{ - public DekId KeyId { get; } = DekId.NewId(); - public EncryptedValue Encrypt(RealmDek key, ReadOnlySpan plain) - { - return new(KeyId, plain.ToArray()); - } - - public byte[] Decrypt(IReadOnlyList keys, EncryptedValue input) - { - return input.Value; - } -} \ No newline at end of file diff --git a/IdentityShroud.TestUtils/Substitutes/NullDekEncryptionService.cs b/IdentityShroud.TestUtils/Substitutes/NullDekEncryptionService.cs deleted file mode 100644 index 84f9cd7..0000000 --- a/IdentityShroud.TestUtils/Substitutes/NullDekEncryptionService.cs +++ /dev/null @@ -1,28 +0,0 @@ -using IdentityShroud.Core.Contracts; -using IdentityShroud.Core.Security; - -namespace IdentityShroud.TestUtils.Substitutes; - -public class NullDekEncryptionService : IDekEncryptionService -{ - public KekId KeyId { get; } = KekId.NewId(); - public EncryptedDek Encrypt(ReadOnlySpan plain) - { - return new(KeyId, plain.ToArray()); - } - - public void Decrypt(EncryptedDek input, Span output) - { - input.Value.CopyTo(output); - } - - public int GetDecryptedSize(EncryptedDek input) - { - return input.Value.Length; - } - - public byte[] Decrypt(EncryptedDek input) - { - return input.Value; - } -} \ No newline at end of file diff --git a/IdentityShroud.sln b/IdentityShroud.sln index b22cf9c..ef65bf2 100644 --- a/IdentityShroud.sln +++ b/IdentityShroud.sln @@ -7,9 +7,6 @@ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IdentityShroud.Core.Tests", "IdentityShroud.Core.Tests\IdentityShroud.Core.Tests.csproj", "{DC887623-8680-4D3B-B23A-D54F7DA91891}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{576359FF-C672-4CC3-A683-3BB9D647E75D}" - ProjectSection(SolutionItems) = preProject - README.md = README.md - EndProjectSection EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IdentityShroud.Migrations", "IdentityShroud.Migrations\IdentityShroud.Migrations.csproj", "{DEECABE3-8934-4696-B0E1-48738DD0CEC4}" EndProject @@ -19,17 +16,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IdentityShroud.TestUtils", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IdentityShroud.TestUtils.Tests", "IdentityShroud.TestUtils.Tests\IdentityShroud.TestUtils.Tests.csproj", "{35D33207-27A8-43E9-A8CA-A158A1E4448C}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "08_Tests", "08_Tests", "{980900AA-E052-498B-A41A-4F33A8678828}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "01", "01", "{07B08872-1141-4BE6-87E6-B85E52FE4341}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IdentityShroud.GraphQL", "IdentityShroud.GraphQL\IdentityShroud.GraphQL.csproj", "{8E9BAD89-B964-4AC2-9773-8CB7E93F76C8}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "04 Plugin Support", "04 Plugin Support", "{ABF0B435-2D50-41C8-847B-0905136537AB}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IdentityShroud.SecretProviders", "IdentityShroud.SecretProviders\IdentityShroud.SecretProviders.csproj", "{A27A70F7-415B-4F06-8A2C-1399675D41AE}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IdentityShroud.PluginSupport", "IdentityShroud.PluginSupport\IdentityShroud.PluginSupport.csproj", "{D4984187-8283-4494-88CF-35EDAD13E4DE}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{980900AA-E052-498B-A41A-4F33A8678828}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -65,28 +52,11 @@ Global {35D33207-27A8-43E9-A8CA-A158A1E4448C}.Debug|Any CPU.Build.0 = Debug|Any CPU {35D33207-27A8-43E9-A8CA-A158A1E4448C}.Release|Any CPU.ActiveCfg = Release|Any CPU {35D33207-27A8-43E9-A8CA-A158A1E4448C}.Release|Any CPU.Build.0 = Release|Any CPU - {8E9BAD89-B964-4AC2-9773-8CB7E93F76C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8E9BAD89-B964-4AC2-9773-8CB7E93F76C8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8E9BAD89-B964-4AC2-9773-8CB7E93F76C8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8E9BAD89-B964-4AC2-9773-8CB7E93F76C8}.Release|Any CPU.Build.0 = Release|Any CPU - {A27A70F7-415B-4F06-8A2C-1399675D41AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A27A70F7-415B-4F06-8A2C-1399675D41AE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A27A70F7-415B-4F06-8A2C-1399675D41AE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A27A70F7-415B-4F06-8A2C-1399675D41AE}.Release|Any CPU.Build.0 = Release|Any CPU - {D4984187-8283-4494-88CF-35EDAD13E4DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D4984187-8283-4494-88CF-35EDAD13E4DE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D4984187-8283-4494-88CF-35EDAD13E4DE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D4984187-8283-4494-88CF-35EDAD13E4DE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {4758FE2E-A437-44F0-B58E-09E52D67D288} = {980900AA-E052-498B-A41A-4F33A8678828} {DC887623-8680-4D3B-B23A-D54F7DA91891} = {980900AA-E052-498B-A41A-4F33A8678828} {35D33207-27A8-43E9-A8CA-A158A1E4448C} = {980900AA-E052-498B-A41A-4F33A8678828} {A8554BCC-C9B6-4D96-90AD-FE80E95441F4} = {980900AA-E052-498B-A41A-4F33A8678828} - {D2B446A0-AB62-4555-9D79-33FF43D7CEF4} = {07B08872-1141-4BE6-87E6-B85E52FE4341} - {8490BF59-B68A-4BE0-9F96-6CB262AF4850} = {07B08872-1141-4BE6-87E6-B85E52FE4341} - {8E9BAD89-B964-4AC2-9773-8CB7E93F76C8} = {07B08872-1141-4BE6-87E6-B85E52FE4341} - {A27A70F7-415B-4F06-8A2C-1399675D41AE} = {ABF0B435-2D50-41C8-847B-0905136537AB} - {D4984187-8283-4494-88CF-35EDAD13E4DE} = {ABF0B435-2D50-41C8-847B-0905136537AB} EndGlobalSection EndGlobal diff --git a/IdentityShroud.sln.DotSettings.user b/IdentityShroud.sln.DotSettings.user index 0e215b5..e39022f 100644 --- a/IdentityShroud.sln.DotSettings.user +++ b/IdentityShroud.sln.DotSettings.user @@ -1,96 +1,36 @@  ForceIncluded ForceIncluded - ForceIncluded - ForceIncluded ForceIncluded - ForceIncluded - ForceIncluded - ForceIncluded ForceIncluded ForceIncluded - ForceIncluded - ForceIncluded - ForceIncluded - ForceIncluded - ForceIncluded - ForceIncluded - ForceIncluded - ForceIncluded - ForceIncluded - ForceIncluded - ForceIncluded ForceIncluded ForceIncluded - ForceIncluded - ForceIncluded ForceIncluded - ForceIncluded - ForceIncluded ForceIncluded - ForceIncluded - ForceIncluded - ForceIncluded - ForceIncluded - ForceIncluded - ForceIncluded ForceIncluded ForceIncluded ForceIncluded ForceIncluded ForceIncluded ForceIncluded - ForceIncluded - ForceIncluded - ForceIncluded ForceIncluded ForceIncluded - ForceIncluded - ForceIncluded - ForceIncluded - ForceIncluded ForceIncluded ForceIncluded - ForceIncluded - ForceIncluded - ForceIncluded - ForceIncluded - ForceIncluded ForceIncluded - - - - - - - - - - + /home/eelke/.cache/JetBrains/Rider2025.3/resharper-host/temp/Rider/vAny/CoverageData/_IdentityShroud.-1277985570/Snapshot/snapshot.utdcvr /home/eelke/.dotnet/dotnet /home/eelke/.dotnet/sdk/10.0.102/MSBuild.dll - <SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from Solution" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> + <SessionState ContinuousTestingMode="0" Name="All tests from Solution" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> <Solution /> </SessionState> + <SessionState ContinuousTestingMode="0" IsActive="True" Name="Junie Session" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> + <ProjectFile>DC887623-8680-4D3B-B23A-D54F7DA91891/d:Services/f:ClientServiceTests.cs</ProjectFile> +</SessionState> - - - - - - - - - - - - - - - - diff --git a/README.md b/README.md deleted file mode 100644 index f4d3cc9..0000000 --- a/README.md +++ /dev/null @@ -1,29 +0,0 @@ -# IdentityShroud - -IdentityShroud is a .NET project for identity management and protection. - -# Architecture - -## App - -Projects combining all into a working app - -## Api - -Supplies Api that can be wired up in a project to get all the required endpoints - -## Core - -Should provide the OIDC functionality with little external dependencies. - -## Infra.* - -Multiple sub projectes. -Provides implementations of the services that core requires. But these can be swapped for custom implementations. - -## Infra.EfCore - -Implementation of services that use an EF core context. - - -