From f99c97f392451dde2bb9e292305b6f1d0d415c4b Mon Sep 17 00:00:00 2001 From: eelke Date: Fri, 6 Feb 2026 19:58:01 +0100 Subject: [PATCH] Miscelanious trials --- .gitignore | 5 + .idea/.idea.IdentityShroud/.idea/.gitignore | 15 +++ .../.idea.IdentityShroud/.idea/encodings.xml | 4 + .../.idea/indexLayout.xml | 8 ++ .idea/.idea.IdentityShroud/.idea/vcs.xml | 6 + .../Docker/compose.generated.override.yml | 21 ++++ .../AppJsonSerializerContext.cs | 8 ++ IdentityShroud.Api/IdentityShroud.Api.csproj | 24 ++++ IdentityShroud.Api/IdentityShroud.http | 11 ++ IdentityShroud.Api/Program.cs | 50 ++++++++ .../Properties/launchSettings.json | 15 +++ IdentityShroud.Api/RealmController.cs | 94 +++++++++++++++ .../appsettings.Development.json | 11 ++ IdentityShroud.Api/appsettings.json | 9 ++ .../IdentityShroud.Core.Tests.csproj | 27 +++++ .../Security/AesGcmHelperTests.cs | 21 ++++ IdentityShroud.Core.Tests/UnitTest1.cs | 107 ++++++++++++++++++ .../Contracts/ISecretProvider.cs | 6 + IdentityShroud.Core/Db.cs | 38 +++++++ .../IdentityShroud.Core.csproj | 21 ++++ IdentityShroud.Core/Messages/JsonWebKey.cs | 34 ++++++ IdentityShroud.Core/Messages/JsonWebKeySet.cs | 9 ++ IdentityShroud.Core/Messages/JsonWebToken.cs | 39 +++++++ .../Messages/OpenIdConfiguration.cs | 72 ++++++++++++ IdentityShroud.Core/Model/Client.cs | 7 ++ IdentityShroud.Core/Model/Realm.cs | 10 ++ IdentityShroud.Core/Security/AesGcmHelper.cs | 64 +++++++++++ .../Security/JwtSignatureGenerator.cs | 38 +++++++ .../DesignTimeDbFactory.cs | 21 ++++ .../IdentityShroud.Migrations.csproj | 20 ++++ IdentityShroud.sln | 39 +++++++ IdentityShroud.sln.DotSettings.user | 14 +++ dotnet-tools.json | 13 +++ 33 files changed, 881 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.idea.IdentityShroud/.idea/.gitignore create mode 100644 .idea/.idea.IdentityShroud/.idea/encodings.xml create mode 100644 .idea/.idea.IdentityShroud/.idea/indexLayout.xml create mode 100644 .idea/.idea.IdentityShroud/.idea/vcs.xml create mode 100644 .idea/.idea.IdentityShroud/Docker/compose.generated.override.yml create mode 100644 IdentityShroud.Api/AppJsonSerializerContext.cs create mode 100644 IdentityShroud.Api/IdentityShroud.Api.csproj create mode 100644 IdentityShroud.Api/IdentityShroud.http create mode 100644 IdentityShroud.Api/Program.cs create mode 100644 IdentityShroud.Api/Properties/launchSettings.json create mode 100644 IdentityShroud.Api/RealmController.cs create mode 100644 IdentityShroud.Api/appsettings.Development.json create mode 100644 IdentityShroud.Api/appsettings.json create mode 100644 IdentityShroud.Core.Tests/IdentityShroud.Core.Tests.csproj create mode 100644 IdentityShroud.Core.Tests/Security/AesGcmHelperTests.cs create mode 100644 IdentityShroud.Core.Tests/UnitTest1.cs create mode 100644 IdentityShroud.Core/Contracts/ISecretProvider.cs create mode 100644 IdentityShroud.Core/Db.cs create mode 100644 IdentityShroud.Core/IdentityShroud.Core.csproj create mode 100644 IdentityShroud.Core/Messages/JsonWebKey.cs create mode 100644 IdentityShroud.Core/Messages/JsonWebKeySet.cs create mode 100644 IdentityShroud.Core/Messages/JsonWebToken.cs create mode 100644 IdentityShroud.Core/Messages/OpenIdConfiguration.cs create mode 100644 IdentityShroud.Core/Model/Client.cs create mode 100644 IdentityShroud.Core/Model/Realm.cs create mode 100644 IdentityShroud.Core/Security/AesGcmHelper.cs create mode 100644 IdentityShroud.Core/Security/JwtSignatureGenerator.cs create mode 100644 IdentityShroud.Migrations/DesignTimeDbFactory.cs create mode 100644 IdentityShroud.Migrations/IdentityShroud.Migrations.csproj create mode 100644 IdentityShroud.sln create mode 100644 IdentityShroud.sln.DotSettings.user create mode 100644 dotnet-tools.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..add57be --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +bin/ +obj/ +/packages/ +riderModule.iml +/_ReSharper.Caches/ \ No newline at end of file diff --git a/.idea/.idea.IdentityShroud/.idea/.gitignore b/.idea/.idea.IdentityShroud/.idea/.gitignore new file mode 100644 index 0000000..98f05d8 --- /dev/null +++ b/.idea/.idea.IdentityShroud/.idea/.gitignore @@ -0,0 +1,15 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Rider ignored files +/modules.xml +/contentModel.xml +/.idea.IdentityShroud.iml +/projectSettingsUpdater.xml +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/.idea.IdentityShroud/.idea/encodings.xml b/.idea/.idea.IdentityShroud/.idea/encodings.xml new file mode 100644 index 0000000..df87cf9 --- /dev/null +++ b/.idea/.idea.IdentityShroud/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/.idea.IdentityShroud/.idea/indexLayout.xml b/.idea/.idea.IdentityShroud/.idea/indexLayout.xml new file mode 100644 index 0000000..7b08163 --- /dev/null +++ b/.idea/.idea.IdentityShroud/.idea/indexLayout.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/.idea.IdentityShroud/.idea/vcs.xml b/.idea/.idea.IdentityShroud/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/.idea.IdentityShroud/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/.idea.IdentityShroud/Docker/compose.generated.override.yml b/.idea/.idea.IdentityShroud/Docker/compose.generated.override.yml new file mode 100644 index 0000000..de3493d --- /dev/null +++ b/.idea/.idea.IdentityShroud/Docker/compose.generated.override.yml @@ -0,0 +1,21 @@ +# This is a generated file. Not intended for manual editing. +services: + identityshroud.api: + build: + context: "/home/eelke/RiderProjects/IdentityShroud" + dockerfile: "IdentityShroud.Api/Dockerfile" + target: "base" + command: [] + entrypoint: + - "dotnet" + - "/app/bin/Debug/net10.0/IdentityShroud.Api.dll" + environment: + ASPNETCORE_ENVIRONMENT: "Development" + DOTNET_USE_POLLING_FILE_WATCHER: "true" + image: "identityshroud.api:dev" + ports: [] + volumes: + - "/home/eelke/RiderProjects/IdentityShroud/IdentityShroud.Api:/app:rw" + - "/home/eelke/RiderProjects/IdentityShroud:/src:rw" + - "/home/eelke/.nuget/packages:/home/app/.nuget/packages" + working_dir: "/app" diff --git a/IdentityShroud.Api/AppJsonSerializerContext.cs b/IdentityShroud.Api/AppJsonSerializerContext.cs new file mode 100644 index 0000000..24d042f --- /dev/null +++ b/IdentityShroud.Api/AppJsonSerializerContext.cs @@ -0,0 +1,8 @@ +using System.Text.Json.Serialization; +using IdentityShroud.Core.Messages; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +[JsonSerializable(typeof(OpenIdConfiguration))] +internal partial class AppJsonSerializerContext : JsonSerializerContext +{ +} \ No newline at end of file diff --git a/IdentityShroud.Api/IdentityShroud.Api.csproj b/IdentityShroud.Api/IdentityShroud.Api.csproj new file mode 100644 index 0000000..232d3ca --- /dev/null +++ b/IdentityShroud.Api/IdentityShroud.Api.csproj @@ -0,0 +1,24 @@ + + + + net10.0 + enable + enable + true + true + Linux + 6b8ef434-0577-4a3c-8749-6b547d7787c5 + + + + + + + + + + + + + + diff --git a/IdentityShroud.Api/IdentityShroud.http b/IdentityShroud.Api/IdentityShroud.http new file mode 100644 index 0000000..390f4ce --- /dev/null +++ b/IdentityShroud.Api/IdentityShroud.http @@ -0,0 +1,11 @@ +@IdentityShroud_HostAddress = http://localhost:5249 + +GET {{IdentityShroud_HostAddress}}/todos/ +Accept: application/json + +### + +GET {{IdentityShroud_HostAddress}}/todos/1 +Accept: application/json + +### diff --git a/IdentityShroud.Api/Program.cs b/IdentityShroud.Api/Program.cs new file mode 100644 index 0000000..cb178ff --- /dev/null +++ b/IdentityShroud.Api/Program.cs @@ -0,0 +1,50 @@ +using IdentityShroud.Api; +using IdentityShroud.Core; +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(); + +var applicationBuilder = WebApplication.CreateSlimBuilder(args); +ConfigureBuilder(applicationBuilder); +var application = applicationBuilder.Build(); +ConfigureApplication(application); +application.Run(); + +void ConfigureBuilder(WebApplicationBuilder builder) +{ + var services = builder.Services; + var configuration = builder.Configuration; + + //services.AddControllers(); + services.ConfigureHttpJsonOptions(options => + { + options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default); + }); + + // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi + services.AddOpenApi(); + services.AddScoped(); + services.AddOptions().Bind(configuration.GetSection("db")); + + builder.Host.UseSerilog((context, services, configuration) => configuration + .Enrich.FromLogContext() + //.Enrich.With() + .ReadFrom.Configuration(context.Configuration)); +} + +void ConfigureApplication(WebApplication app) +{ + if (app.Environment.IsDevelopment()) + { + app.MapOpenApi(); + } + app.UseSerilogRequestLogging(); + app.MapRealmEndpoints(); + // app.UseRouting(); + // app.MapControllers(); +} diff --git a/IdentityShroud.Api/Properties/launchSettings.json b/IdentityShroud.Api/Properties/launchSettings.json new file mode 100644 index 0000000..9472c5a --- /dev/null +++ b/IdentityShroud.Api/Properties/launchSettings.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "todos", + "applicationUrl": "http://localhost:5249", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/IdentityShroud.Api/RealmController.cs b/IdentityShroud.Api/RealmController.cs new file mode 100644 index 0000000..023e251 --- /dev/null +++ b/IdentityShroud.Api/RealmController.cs @@ -0,0 +1,94 @@ +using IdentityShroud.Core.Messages; +using Microsoft.AspNetCore.Http.HttpResults; + +namespace IdentityShroud.Api; + +public static class RealmController +{ + public static void MapRealmEndpoints(this IEndpointRouteBuilder app) + { + var realm = app.MapGroup("/realms/{slug}"); + + realm.MapGet("", GetRoot); + realm.MapGet(".well-known/openid-configuration", GetOpenIdConfiguration); + + var openidConnect = realm.MapGroup("openid-connect"); + openidConnect.MapPost("auth", OpenIdConnectAuth); + openidConnect.MapPost("token", OpenIdConnectToken); + openidConnect.MapGet("jwks", OpenIdConnectJwks); + } + + private static Task OpenIdConnectJwks(HttpContext context) + { + throw new NotImplementedException(); + } + + private static Task OpenIdConnectToken(HttpContext context) + { + throw new NotImplementedException(); + } + + private static Task OpenIdConnectAuth(HttpContext context) + { + throw new NotImplementedException(); + } + + private static async Task, BadRequest>> GetOpenIdConfiguration(string slug, HttpContext context) + { + if (string.IsNullOrEmpty(slug)) + return TypedResults.BadRequest(); + var s = $"{context.Request.Scheme}://{context.Request.Host}{context.Request.Path}"; + var searchString = $"realms/{slug}"; + int index = s.IndexOf(searchString, StringComparison.OrdinalIgnoreCase); + string baseUri = s.Substring(0, index + searchString.Length); + + return TypedResults.Json(new OpenIdConfiguration() + { + AuthorizationEndpoint = baseUri + "/openid-connect/auth", + TokenEndpoint = baseUri + "/openid-connect/token", + Issuer = baseUri, + JwksUri = baseUri + "/openid-connect/jwks", + }, AppJsonSerializerContext.Default.OpenIdConfiguration); + } + + private static string GetRoot() + { + return "Hello World!"; + + /* keycloak returns this + { + "realm": "mpluskassa", + "public_key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApYbLAeOLDEwzL4tEwuE2LfisOBXoQqWA9RdP3ph6muwF1ErfhiBSIB2JETKf7F1OsiF1/qnuh4uDfn0TO8bK3lSfHTlIHWShwaJ/UegS9ylobfIYXJsz0xmJK5ToFaSYa72D/Dyln7ROxudu8+zc70sz7bUKQ0/ktWRsiu76vY6Kr9+18PgaooPmb2QP8lS8IZEv+gW5SLqoMc1DfD8lsih1sdnQ8W65cBsNnenkWc97AF9cMR6rdD2tZfLAxEHKYaohAL9EsQsLic3P2f2UaqRTAOvgqyYE5hyJROt7Pyeyi8YSy7zXD12h2mc0mrSoA+u7s/GrOLcLoLLgEnRRVwIDAQAB", + "token-service": "https://iam.kassacloud.nl/auth/realms/mpluskassa/protocol/openid-connect", + "account-service": "https://iam.kassacloud.nl/auth/realms/mpluskassa/account", + "tokens-not-before": 0 + } + */ + } + + // [HttpGet("")] + // public ActionResult Index() + // { + // return new JsonResult("Hello world!"); + // } + + // [HttpGet("{slug}/.well-known/openid-configuration")] + // public ActionResult GetOpenIdConfiguration( + // string slug, + // [FromServices]LinkGenerator linkGenerator) + // { + // var s = $"{HttpContext.Request.Scheme}://{HttpContext.Request.Host}{HttpContext.Request.Path}"; + // var searchString = $"realms/{slug}"; + // int index = s.IndexOf(searchString, StringComparison.OrdinalIgnoreCase); + // string baseUri = s.Substring(0, index + searchString.Length); + // + // return new JsonResult(baseUri); + // } + + // [HttpPost("{slug}/protocol/openid-connect/token")] + // public ActionResult GetOpenIdConnectToken(string slug) + // + // { + // return new JsonResult("Hello world!"); + // } +} \ No newline at end of file diff --git a/IdentityShroud.Api/appsettings.Development.json b/IdentityShroud.Api/appsettings.Development.json new file mode 100644 index 0000000..e5dce75 --- /dev/null +++ b/IdentityShroud.Api/appsettings.Development.json @@ -0,0 +1,11 @@ +{ + "Db": { + "LogSensitiveData": true + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/IdentityShroud.Api/appsettings.json b/IdentityShroud.Api/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/IdentityShroud.Api/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/IdentityShroud.Core.Tests/IdentityShroud.Core.Tests.csproj b/IdentityShroud.Core.Tests/IdentityShroud.Core.Tests.csproj new file mode 100644 index 0000000..02a3ef9 --- /dev/null +++ b/IdentityShroud.Core.Tests/IdentityShroud.Core.Tests.csproj @@ -0,0 +1,27 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/IdentityShroud.Core.Tests/Security/AesGcmHelperTests.cs b/IdentityShroud.Core.Tests/Security/AesGcmHelperTests.cs new file mode 100644 index 0000000..6392676 --- /dev/null +++ b/IdentityShroud.Core.Tests/Security/AesGcmHelperTests.cs @@ -0,0 +1,21 @@ +using System.Security.Cryptography; +using System.Text; +using IdentityShroud.Core.Security; + +namespace IdentityShroud.Core.Tests.Security; + +public class AesGcmHelperTests +{ + [Fact] + public void EncryptDecryptCycleWorks() + { + string input = "Hello, world!"; + + var encryptionKey = RandomNumberGenerator.GetBytes(32); + + var cypher = AesGcmHelper.EncryptAesGcm(Encoding.UTF8.GetBytes(input), encryptionKey); + var output = AesGcmHelper.DecryptAesGcm(cypher, encryptionKey); + + Assert.Equal(input, Encoding.UTF8.GetString(output)); + } +} \ No newline at end of file diff --git a/IdentityShroud.Core.Tests/UnitTest1.cs b/IdentityShroud.Core.Tests/UnitTest1.cs new file mode 100644 index 0000000..a998f4a --- /dev/null +++ b/IdentityShroud.Core.Tests/UnitTest1.cs @@ -0,0 +1,107 @@ +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using IdentityShroud.Core.Messages; +using Microsoft.AspNetCore.WebUtilities; + +namespace IdentityShroud.Core.Tests; + +public class UnitTest1 +{ + private const string TestJwt = @"eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJybVZ3TU5rM0o1WHlmMWhyS3NVbEVYN1BNUm42dlZKY0h3U3FYMUVQRnFJIn0.eyJleHAiOjE3Njk5MzY5MDksImlhdCI6MTc2OTkzNjYwOSwianRpIjoiMjNiZDJmNjktODdhYi00YmM2LWE0MWQtZGZkNzkxNDc4ZDM0IiwiaXNzIjoiaHR0cHM6Ly9pYW0ua2Fzc2FjbG91ZC5ubC9hdXRoL3JlYWxtcy9tcGx1c2thc3NhIiwiYXVkIjpbImthc3NhLW1hbmFnZW1lbnQtc2VydmljZSIsImFwYWNoZTItaW50cmFuZXQtYXV0aCIsImFjY291bnQiXSwic3ViIjoiMDkzY2NmMTUtYzRhOS00YWI0LTk3MWYtZDVhMDIyMzZkODVhIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoibXBvYmFja2VuZCIsInNpZCI6IjI2NmUyNjJiLTU5NjMtNDUyZi04ZTI3LWIwZTkzMjBkNTZkNiIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJkZWZhdWx0LXJvbGVzLW1wbHVza2Fzc2EiLCJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIiwiZGVhbGVyLW1lZGV3ZXJrZXItcm9sZSIsIm1wbHVza2Fzc2EtbWVkZXdlcmtlci1yb2xlIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYXBhY2hlMi1pbnRyYW5ldC1hdXRoIjp7InJvbGVzIjpbImludHJhbmV0IiwicmVsZWFzZW5vdGVzX3dyaXRlIl19LCJrYXNzYS1tYW5hZ2VtZW50LXNlcnZpY2UiOnsicm9sZXMiOlsicG9zYWNjb3VudF9wYXNzd29yZHJlc2V0IiwiZHJhZnRfbGljZW5zZV93cml0ZSIsImxpY2Vuc2VfcmVhZCIsImtub3dsZWRnZUl0ZW1fcmVhZCIsIm1haWxpbmdfcmVhZCIsIm1wbHVzYXBpX3JlYWQiLCJkYXRhYmFzZV91c2VyX3dyaXRlIiwiZW52aXJvbm1lbnRfd3JpdGUiLCJna3NfYXV0aGNvZGVfcmVhZCIsImVtcGxveWVlX3JlYWQiLCJkYXRhYmFzZV91c2VyX3JlYWQiLCJhcGlhY2NvdW50X3Bhc3N3b3JkcmVzZXQiLCJtcGx1c2FwaV93cml0ZSIsImVudmlyb25tZW50X3JlYWQiLCJrbm93bGVkZ2VJdGVtX3dyaXRlIiwiZGF0YWJhc2VfdXNlcl9wYXNzd29yZF9yZWFkIiwibGljZW5zZV93cml0ZSIsImN1c3RvbWVyX3dyaXRlIiwiZGVhbGVyX3JlYWQiLCJlbXBsb3llZV93cml0ZSIsImRhdGFiYXNlX2NvbmZpZ3VyYXRpb25fd3JpdGUiLCJyZWxhdGlvbnNfcmVhZCIsImRhdGFiYXNlX3VzZXJfcGFzc3dvcmRfbXBsdXNfZW5jcnlwdGVkX3JlYWQiLCJkcmFmdF9saWNlbnNlX3JlYWQiLCJkYXRhYmFzZV9jb25maWd1cmF0aW9uX3JlYWQiXX0sImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoia21zIGVtYWlsIHByb2ZpbGUiLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiZGVhbGVySWQiOjEsIm5hbWUiOiJFZWxrZSBLbGVpbiIsInByZWZlcnJlZF91c2VybmFtZSI6ImVlbGtlQGJvbHQubmwiLCJsb2NhbGUiOiJlbiIsImdpdmVuX25hbWUiOiJFZWxrZSIsImZhbWlseV9uYW1lIjoiS2xlaW4iLCJlbWFpbCI6ImVlbGtlQGJvbHQubmwiLCJlbXBsb3llZU51bWJlciI6NTR9.Z1mjZkpFLIMsx2EWRNwFXYinwUO4iRmteClGWoj70c9AffhjEN5hmSL2ErLn9RjofODY5JovbDo3RDBrTWMDdGHYavxaZRzn8EJe6Ndp9b2n7kUNHJpBrqIGEMhD5sY_5YRTfMIe7j7k2oRW8QIpcQ5_bYUBKEWfHYQqfi8IfpRjLhgd6zKMC3Faj4e442p5zY2dEdWHEr5j6--_Py4HhNsomtTY6WPFH8nTCJ9pbMM9ThSDmdjbSiMbNLeSAQzJNVXp5GGsSVlkJNRlMI_Mmfd7jWpSDZpNPzxJ-HmkDhY9oiInZW0livnf9-eesKWljO3TAXKEuiqigsblSbLjsQ"; + [Fact] + public void DecodeTest() + { + var decoded = JwtReader.Decode(TestJwt); + + Assert.Equal("RS256", decoded.Header.Algorithm); + Assert.Equal("https://iam.kassacloud.nl/auth/realms/mpluskassa", decoded.Payload.Issuer); + } + + [Fact] + public void CreateTest() + { + using (RSA rsa = RSA.Create()) + { + // Option 1: Load from PEM string + // string privateKeyPem = @"-----BEGIN PRIVATE KEY----- + // ... your key here ... + // -----END PRIVATE KEY-----"; + // rsa.ImportFromPem(privateKeyPem); + + // Option 2: Load from XML + // string xmlKey = "..."; + // rsa.FromXmlString(xmlKey); + + // Option 3: Generate a new key for testing + rsa.KeySize = 2048; + + // Your already encoded header and payload + string header = "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJybVZ3TU5rM0o1WHlmMWhyS3NVbEVYN1BNUm42dlZKY0h3U3FYMUVQRnFJIn0"; + string payload = "eyJleHAiOjE3Njk5MzY5MDksImlhdCI6MTc2OTkzNjYwOSwianRpIjoiMjNiZDJmNjktODdhYi00YmM2LWE0MWQtZGZkNzkxNDc4ZDM0IiwiaXNzIjoiaHR0cHM6Ly9pYW0ua2Fzc2FjbG91ZC5ubC9hdXRoL3JlYWxtcy9tcGx1c2thc3NhIiwiYXVkIjpbImthc3NhLW1hbmFnZW1lbnQtc2VydmljZSIsImFwYWNoZTItaW50cmFuZXQtYXV0aCIsImFjY291bnQiXSwic3ViIjoiMDkzY2NmMTUtYzRhOS00YWI0LTk3MWYtZDVhMDIyMzZkODVhIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoibXBvYmFja2VuZCIsInNpZCI6IjI2NmUyNjJiLTU5NjMtNDUyZi04ZTI3LWIwZTkzMjBkNTZkNiIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJkZWZhdWx0LXJvbGVzLW1wbHVza2Fzc2EiLCJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIiwiZGVhbGVyLW1lZGV3ZXJrZXItcm9sZSIsIm1wbHVza2Fzc2EtbWVkZXdlcmtlci1yb2xlIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYXBhY2hlMi1pbnRyYW5ldC1hdXRoIjp7InJvbGVzIjpbImludHJhbmV0IiwicmVsZWFzZW5vdGVzX3dyaXRlIl19LCJrYXNzYS1tYW5hZ2VtZW50LXNlcnZpY2UiOnsicm9sZXMiOlsicG9zYWNjb3VudF9wYXNzd29yZHJlc2V0IiwiZHJhZnRfbGljZW5zZV93cml0ZSIsImxpY2Vuc2VfcmVhZCIsImtub3dsZWRnZUl0ZW1fcmVhZCIsIm1haWxpbmdfcmVhZCIsIm1wbHVzYXBpX3JlYWQiLCJkYXRhYmFzZV91c2VyX3dyaXRlIiwiZW52aXJvbm1lbnRfd3JpdGUiLCJna3NfYXV0aGNvZGVfcmVhZCIsImVtcGxveWVlX3JlYWQiLCJkYXRhYmFzZV91c2VyX3JlYWQiLCJhcGlhY2NvdW50X3Bhc3N3b3JkcmVzZXQiLCJtcGx1c2FwaV93cml0ZSIsImVudmlyb25tZW50X3JlYWQiLCJrbm93bGVkZ2VJdGVtX3dyaXRlIiwiZGF0YWJhc2VfdXNlcl9wYXNzd29yZF9yZWFkIiwibGljZW5zZV93cml0ZSIsImN1c3RvbWVyX3dyaXRlIiwiZGVhbGVyX3JlYWQiLCJlbXBsb3llZV93cml0ZSIsImRhdGFiYXNlX2NvbmZpZ3VyYXRpb25fd3JpdGUiLCJyZWxhdGlvbnNfcmVhZCIsImRhdGFiYXNlX3VzZXJfcGFzc3dvcmRfbXBsdXNfZW5jcnlwdGVkX3JlYWQiLCJkcmFmdF9saWNlbnNlX3JlYWQiLCJkYXRhYmFzZV9jb25maWd1cmF0aW9uX3JlYWQiXX0sImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoia21zIGVtYWlsIHByb2ZpbGUiLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiZGVhbGVySWQiOjEsIm5hbWUiOiJFZWxrZSBLbGVpbiIsInByZWZlcnJlZF91c2VybmFtZSI6ImVlbGtlQGJvbHQubmwiLCJsb2NhbGUiOiJlbiIsImdpdmVuX25hbWUiOiJFZWxrZSIsImZhbWlseV9uYW1lIjoiS2xlaW4iLCJlbWFpbCI6ImVlbGtlQGJvbHQubmwiLCJlbXBsb3llZU51bWJlciI6NTR9"; + + // Generate signature + string signature = JwtSignatureGenerator.GenerateRS256Signature(header, payload, rsa); + + string publicKeyPem = rsa.ExportSubjectPublicKeyInfoPem(); + string token = $"{header}.{payload}.{signature}"; + + Console.WriteLine($"Signature: {signature}"); + + // Or generate complete JWT + // string completeJwt = JwtSignatureGenerator.GenerateCompleteJwt(header, payload, rsa); + // Console.WriteLine($"Complete JWT: {completeJwt}"); + } + } +} + +public static class JwtReader +{ + + public static JsonWebToken Decode(string jwt) + { + // there should only be two, if not one of the base64 decode calls should fail. + int firstDot = jwt.IndexOf('.'); + int secondDot = jwt.LastIndexOf('.'); + return new JsonWebToken() + { + Header = JsonSerializer.Deserialize( + Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(jwt, 0, firstDot))), + Payload = JsonSerializer.Deserialize( + Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(jwt, firstDot + 1, secondDot - (firstDot + 1)))), + Signature = WebEncoders.Base64UrlDecode(jwt, secondDot + 1, jwt.Length - (secondDot + 1)) + }; + } +} + +public static class RsaKeyLoader +{ + /// + /// Load RSA private key from PEM format (.NET 5+) + /// + public static RSA LoadFromPem(string pemKey) + { + var rsa = RSA.Create(); + rsa.ImportFromPem(pemKey); + return rsa; + } + + /// + /// Load RSA private key from file + /// + public static RSA LoadFromPemFile(string filePath) + { + string pemContent = System.IO.File.ReadAllText(filePath); + return LoadFromPem(pemContent); + } + + /// + /// Load RSA private key from PKCS#8 format + /// + public static RSA LoadFromPkcs8(byte[] pkcs8Key) + { + var rsa = RSA.Create(); + rsa.ImportPkcs8PrivateKey(pkcs8Key, out _); + return rsa; + } +} \ No newline at end of file diff --git a/IdentityShroud.Core/Contracts/ISecretProvider.cs b/IdentityShroud.Core/Contracts/ISecretProvider.cs new file mode 100644 index 0000000..8a1042b --- /dev/null +++ b/IdentityShroud.Core/Contracts/ISecretProvider.cs @@ -0,0 +1,6 @@ +namespace IdentityShroud.Core.Contracts; + +public interface ISecretProvider +{ + Task GetSecretAsync(string name); +} diff --git a/IdentityShroud.Core/Db.cs b/IdentityShroud.Core/Db.cs new file mode 100644 index 0000000..2f95902 --- /dev/null +++ b/IdentityShroud.Core/Db.cs @@ -0,0 +1,38 @@ +using IdentityShroud.Core.Model; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace IdentityShroud.Core; + +public class DbConfiguration +{ + public string ConnectionString { get; set; } = ""; + public bool LogSensitiveData { get; set; } = false; +} + +public class Db( + IOptions configuration, + ILoggerFactory? loggerFactory) + : DbContext +{ + public virtual DbSet Realms { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseNpgsql(""); + optionsBuilder.UseNpgsql( + configuration.Value.ConnectionString, + o => o.MigrationsAssembly("IdentityShroud.Migrations")); // , o => o.UseNodaTime().UseVector().MigrationsAssembly("Migrations.KnowledgeBaseDB")); + optionsBuilder.UseSnakeCaseNamingConvention(); + + if (configuration.Value.LogSensitiveData) + optionsBuilder.EnableSensitiveDataLogging(); + + if (loggerFactory is { } ) + { + optionsBuilder.UseLoggerFactory(loggerFactory); + } + + } +} \ No newline at end of file diff --git a/IdentityShroud.Core/IdentityShroud.Core.csproj b/IdentityShroud.Core/IdentityShroud.Core.csproj new file mode 100644 index 0000000..9949c5a --- /dev/null +++ b/IdentityShroud.Core/IdentityShroud.Core.csproj @@ -0,0 +1,21 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + ..\..\..\.nuget\packages\microsoft.aspnetcore.webutilities\10.0.2\lib\net10.0\Microsoft.AspNetCore.WebUtilities.dll + + + + diff --git a/IdentityShroud.Core/Messages/JsonWebKey.cs b/IdentityShroud.Core/Messages/JsonWebKey.cs new file mode 100644 index 0000000..e22a899 --- /dev/null +++ b/IdentityShroud.Core/Messages/JsonWebKey.cs @@ -0,0 +1,34 @@ +using System.Text.Json.Serialization; + +namespace IdentityShroud.Core.Messages; + +public class JsonWebKey +{ + [JsonPropertyName("kty")] + public string KeyType { get; set; } = "RSA"; + + [JsonPropertyName("use")] + public string Use { get; set; } = "sig"; // "sig" for signature, "enc" for encryption + + [JsonPropertyName("alg")] + public string Algorithm { get; set; } = "RS256"; + + [JsonPropertyName("kid")] + public string KeyId { get; set; } + + // RSA Public Key Components + [JsonPropertyName("n")] + public string Modulus { get; set; } + + [JsonPropertyName("e")] + public string Exponent { get; set; } + + // Optional fields + [JsonPropertyName("x5c")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List X509CertificateChain { get; set; } + + [JsonPropertyName("x5t")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string X509CertificateThumbprint { get; set; } +} \ No newline at end of file diff --git a/IdentityShroud.Core/Messages/JsonWebKeySet.cs b/IdentityShroud.Core/Messages/JsonWebKeySet.cs new file mode 100644 index 0000000..a47a617 --- /dev/null +++ b/IdentityShroud.Core/Messages/JsonWebKeySet.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace IdentityShroud.Core.Messages; + +public class JsonWebKeySet +{ + [JsonPropertyName("keys")] + public List Keys { get; set; } = new List(); +} \ No newline at end of file diff --git a/IdentityShroud.Core/Messages/JsonWebToken.cs b/IdentityShroud.Core/Messages/JsonWebToken.cs new file mode 100644 index 0000000..65d671f --- /dev/null +++ b/IdentityShroud.Core/Messages/JsonWebToken.cs @@ -0,0 +1,39 @@ +using System.Text.Json.Serialization; + +namespace IdentityShroud.Core.Messages; + +public class JsonWebTokenHeader +{ + [JsonPropertyName("alg")] + public string Algorithm { get; set; } = "HS256"; + [JsonPropertyName("typ")] + public string Type { get; set; } = "JWT"; + [JsonPropertyName("kid")] + public string KeyId { get; set; } + +} + +public class JsonWebTokenPayload +{ + [JsonPropertyName("iss")] + public string Issuer { get; set; } + [JsonPropertyName("aud")] + public string[] Audience { get; set; } + [JsonPropertyName("sub")] + public string Subject { get; set; } + [JsonPropertyName("exp")] + public long Expires { get; set; } + [JsonPropertyName("iat")] + public long IssuedAt { get; set; } + [JsonPropertyName("nbf")] + public long NotBefore { get; set; } + [JsonPropertyName("jti")] + public Guid JwtId { get; set; } +} + +public class JsonWebToken +{ + public JsonWebTokenHeader Header { get; set; } = new(); + public JsonWebTokenPayload Payload { get; set; } = new(); + public byte[] Signature { get; set; } = []; +} \ No newline at end of file diff --git a/IdentityShroud.Core/Messages/OpenIdConfiguration.cs b/IdentityShroud.Core/Messages/OpenIdConfiguration.cs new file mode 100644 index 0000000..05d1a30 --- /dev/null +++ b/IdentityShroud.Core/Messages/OpenIdConfiguration.cs @@ -0,0 +1,72 @@ +using System.Text.Json.Serialization; + +namespace IdentityShroud.Core.Messages; + +/// +/// https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata +/// +public class OpenIdConfiguration +{ + /// + /// REQUIRED. URL using the https scheme with no query or fragment components that the OP asserts as its + /// Issuer Identifier. If Issuer discovery is supported (see Section 2), this value MUST be identical to the + /// issuer value returned by WebFinger. This also MUST be identical to the iss Claim value in ID Tokens issued + /// from this Issuer. + /// + [JsonPropertyName("issuer")] + public required string Issuer { get; set; } + /// + /// REQUIRED. URL of the OP's OAuth 2.0 Authorization Endpoint [OpenID.Core]. This URL MUST use the https scheme + /// and MAY contain port, path, and query parameter components. + /// + [JsonPropertyName("authorization_endpoint")] + public required string AuthorizationEndpoint { get; set; } + /// + /// URL of the OP's OAuth 2.0 Token Endpoint [OpenID.Core]. This is REQUIRED unless only the Implicit Flow is used. + /// This URL MUST use the https scheme and MAY contain port, path, and query parameter components. + /// + [JsonPropertyName("token_endpoint")] + public string? TokenEndpoint { get; set; } + /// + /// RECOMMENDED. URL of the OP's UserInfo Endpoint [OpenID.Core]. This URL MUST use the https scheme and MAY contain + /// port, path, and query parameter components. + /// + [JsonPropertyName("userinfo_endpoint")] + public string? UserInfoEndpoint { get; set; } + + /// + /// REQUIRED. URL of the OP's JWK Set [JWK] document, which MUST use the https scheme. This contains the signing + /// key(s) the RP uses to validate signatures from the OP. The JWK Set MAY also contain the Server's encryption + /// key(s), which are used by RPs to encrypt requests to the Server. When both signing and encryption keys are made + /// available, a use (public key use) parameter value is REQUIRED for all keys in the referenced JWK Set to indicate + /// each key's intended usage. Although some algorithms allow the same key to be used for both signatures and + /// encryption, doing so is NOT RECOMMENDED, as it is less secure. The JWK x5c parameter MAY be used to provide + /// X.509 representations of keys provided. When used, the bare key values MUST still be present and MUST match + /// those in the certificate. The JWK Set MUST NOT contain private or symmetric key values. + /// + [JsonPropertyName("jwks_uri")] + public required string JwksUri { get; set; } + + /// + /// REQUIRED. JSON array containing a list of the OAuth 2.0 response_type values that this OP supports. Dynamic + /// OpenID Providers MUST support the code, id_token, and the id_token token Response Type values. + /// + [JsonPropertyName("response_types_supported")] + public string[] ResponseTypesSupported { get; set; } = [ "code", "id_token", "id_token token"]; + + /// + /// REQUIRED. JSON array containing a list of the Subject Identifier types that this OP supports. Valid types + /// include pairwise and public. + /// + [JsonPropertyName("subject_types_supported")] + public string[] SubjectTypesSupported { get; set; } = [ "public" ]; + + /// + /// REQUIRED. JSON array containing a list of the JWS signing algorithms (alg values) supported by the OP for the + /// ID Token to encode the Claims in a JWT [JWT]. The algorithm RS256 MUST be included. The value none MAY be + /// supported but MUST NOT be used unless the Response Type used returns no ID Token from the Authorization + /// Endpoint (such as when using the Authorization Code Flow). + /// + [JsonPropertyName("id_token_signing_alg_values_supported")] + public string[] IdTokenSigningAlgValuesSupported { get; set; } = [ "RS256" ]; +} \ No newline at end of file diff --git a/IdentityShroud.Core/Model/Client.cs b/IdentityShroud.Core/Model/Client.cs new file mode 100644 index 0000000..0be04ed --- /dev/null +++ b/IdentityShroud.Core/Model/Client.cs @@ -0,0 +1,7 @@ +namespace IdentityShroud.Core.Model; + +public class Client +{ + public Guid Id { get; set; } + public string Name { get; set; } +} \ No newline at end of file diff --git a/IdentityShroud.Core/Model/Realm.cs b/IdentityShroud.Core/Model/Realm.cs new file mode 100644 index 0000000..458ad59 --- /dev/null +++ b/IdentityShroud.Core/Model/Realm.cs @@ -0,0 +1,10 @@ +namespace IdentityShroud.Core.Model; + +public class Realm +{ + public Guid Id { get; set; } + public string Slug { get; set; } = ""; + public string Description { get; set; } = ""; + public List Clients { get; set; } = []; + public byte[] PrivateKey { get; set; } +} \ No newline at end of file diff --git a/IdentityShroud.Core/Security/AesGcmHelper.cs b/IdentityShroud.Core/Security/AesGcmHelper.cs new file mode 100644 index 0000000..1f0e9de --- /dev/null +++ b/IdentityShroud.Core/Security/AesGcmHelper.cs @@ -0,0 +1,64 @@ +using System.Security.Cryptography; + +namespace IdentityShroud.Core.Security; + +public static class AesGcmHelper +{ + + public static byte[] EncryptAesGcm(byte[] plaintext, byte[] key) + { + using var aes = new AesGcm(key); + byte[] nonce = RandomNumberGenerator.GetBytes(AesGcm.NonceByteSizes.MaxSize); + byte[] ciphertext = new byte[plaintext.Length]; + byte[] tag = new byte[AesGcm.TagByteSizes.MaxSize]; + + aes.Encrypt(nonce, plaintext, ciphertext, tag); + // Return concatenated nonce|ciphertext|tag (or store separately) + return nonce.Concat(ciphertext).Concat(tag).ToArray(); + } + + // -------------------------------------------------------------------- + // DecryptAesGcm + // • key – 32‑byte (256‑bit) secret key (same key used for encryption) + // • payload – byte[] containing nonce‖ciphertext‖tag + // • returns – the original plaintext bytes + // -------------------------------------------------------------------- + public static byte[] DecryptAesGcm(byte[] payload, byte[] key) + { + if (payload == null) throw new ArgumentNullException(nameof(payload)); + if (key == null) throw new ArgumentNullException(nameof(key)); + if (key.Length != 32) // 256‑bit key + throw new ArgumentException("Key must be 256 bits (32 bytes) for AES‑256‑GCM.", nameof(key)); + + // ---------------------------------------------------------------- + // 1️⃣ Extract the three components. + // ---------------------------------------------------------------- + // AesGcm.NonceByteSizes.MaxSize = 12 bytes (standard GCM nonce length) + // AesGcm.TagByteSizes.MaxSize = 16 bytes (128‑bit authentication tag) + int nonceSize = AesGcm.NonceByteSizes.MaxSize; // 12 + int tagSize = AesGcm.TagByteSizes.MaxSize; // 16 + + if (payload.Length < nonceSize + tagSize) + throw new ArgumentException("Payload is too short to contain nonce, ciphertext, and tag.", nameof(payload)); + + ReadOnlySpan nonce = new(payload, 0, nonceSize); + ReadOnlySpan ciphertext = new(payload, nonceSize, payload.Length - nonceSize - tagSize); + ReadOnlySpan tag = new(payload, payload.Length - tagSize, tagSize); + + + byte[] plaintext = new byte[ciphertext.Length]; + + using var aes = new AesGcm(key); + try + { + aes.Decrypt(nonce, ciphertext, tag, plaintext); + } + catch (CryptographicException ex) + { + // Tag verification failed → tampering or wrong key/nonce. + throw new InvalidOperationException("Decryption failed – authentication tag mismatch.", ex); + } + + return plaintext; + } +} \ No newline at end of file diff --git a/IdentityShroud.Core/Security/JwtSignatureGenerator.cs b/IdentityShroud.Core/Security/JwtSignatureGenerator.cs new file mode 100644 index 0000000..11f8dc2 --- /dev/null +++ b/IdentityShroud.Core/Security/JwtSignatureGenerator.cs @@ -0,0 +1,38 @@ +using System.Security.Cryptography; +using System.Text; +using Microsoft.AspNetCore.WebUtilities; + +namespace IdentityShroud.Core; + +public class JwtSignatureGenerator +{ + /// + /// Generates a JWT signature using RS256 algorithm + /// + /// Base64Url encoded header + /// Base64Url encoded payload + /// RSA private key (PEM format or RSA parameters) + /// Base64Url encoded signature + public static string GenerateRS256Signature(string headerBase64Url, string payloadBase64Url, RSA privateKey) + { + // Combine header and payload with a period + string dataToSign = $"{headerBase64Url}.{payloadBase64Url}"; + + // Convert to bytes + byte[] dataBytes = Encoding.UTF8.GetBytes(dataToSign); + + // Sign the data using RSA-SHA256 + byte[] signatureBytes = privateKey.SignData(dataBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + + // Convert signature to Base64Url encoding + string signature = WebEncoders.Base64UrlEncode(signatureBytes); + + return signature; + } + + public static string GenerateCompleteJwt(string headerBase64Url, string payloadBase64Url, RSA privateKey) + { + string signature = GenerateRS256Signature(headerBase64Url, payloadBase64Url, privateKey); + return $"{headerBase64Url}.{payloadBase64Url}.{signature}"; + } +} \ No newline at end of file diff --git a/IdentityShroud.Migrations/DesignTimeDbFactory.cs b/IdentityShroud.Migrations/DesignTimeDbFactory.cs new file mode 100644 index 0000000..9459610 --- /dev/null +++ b/IdentityShroud.Migrations/DesignTimeDbFactory.cs @@ -0,0 +1,21 @@ +using IdentityShroud.Core; +using Microsoft.EntityFrameworkCore.Design; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; + +namespace IdentityShroud.Migrations; + +public sealed class DesignTimeDbFactory : IDesignTimeDbContextFactory +{ + public Db CreateDbContext(string[] args) + { + // You can load from env/args/user-secrets if you like; keep placeholders out of source control. + var cfg = Options.Create(new DbConfiguration + { + ConnectionString = "Host=localhost;Port=5432;Database=identityshroud;Username=identityshroud;Password=enshrouded;SSL Mode=allow;Trust Server Certificate=true", + LogSensitiveData = false + }); + + return new Db(cfg, NullLoggerFactory.Instance); + } +} \ No newline at end of file diff --git a/IdentityShroud.Migrations/IdentityShroud.Migrations.csproj b/IdentityShroud.Migrations/IdentityShroud.Migrations.csproj new file mode 100644 index 0000000..f4583e2 --- /dev/null +++ b/IdentityShroud.Migrations/IdentityShroud.Migrations.csproj @@ -0,0 +1,20 @@ + + + + net10.0 + enable + enable + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/IdentityShroud.sln b/IdentityShroud.sln new file mode 100644 index 0000000..7d9329f --- /dev/null +++ b/IdentityShroud.sln @@ -0,0 +1,39 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IdentityShroud.Api", "IdentityShroud.Api\IdentityShroud.Api.csproj", "{D2B446A0-AB62-4555-9D79-33FF43D7CEF4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IdentityShroud.Core", "IdentityShroud.Core\IdentityShroud.Core.csproj", "{8490BF59-B68A-4BE0-9F96-6CB262AF4850}" +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 + compose.yaml = compose.yaml + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IdentityShroud.Migrations", "IdentityShroud.Migrations\IdentityShroud.Migrations.csproj", "{DEECABE3-8934-4696-B0E1-48738DD0CEC4}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D2B446A0-AB62-4555-9D79-33FF43D7CEF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D2B446A0-AB62-4555-9D79-33FF43D7CEF4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D2B446A0-AB62-4555-9D79-33FF43D7CEF4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D2B446A0-AB62-4555-9D79-33FF43D7CEF4}.Release|Any CPU.Build.0 = Release|Any CPU + {8490BF59-B68A-4BE0-9F96-6CB262AF4850}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8490BF59-B68A-4BE0-9F96-6CB262AF4850}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8490BF59-B68A-4BE0-9F96-6CB262AF4850}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8490BF59-B68A-4BE0-9F96-6CB262AF4850}.Release|Any CPU.Build.0 = Release|Any CPU + {DC887623-8680-4D3B-B23A-D54F7DA91891}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DC887623-8680-4D3B-B23A-D54F7DA91891}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DC887623-8680-4D3B-B23A-D54F7DA91891}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DC887623-8680-4D3B-B23A-D54F7DA91891}.Release|Any CPU.Build.0 = Release|Any CPU + {DEECABE3-8934-4696-B0E1-48738DD0CEC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DEECABE3-8934-4696-B0E1-48738DD0CEC4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DEECABE3-8934-4696-B0E1-48738DD0CEC4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DEECABE3-8934-4696-B0E1-48738DD0CEC4}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/IdentityShroud.sln.DotSettings.user b/IdentityShroud.sln.DotSettings.user new file mode 100644 index 0000000..3f70f92 --- /dev/null +++ b/IdentityShroud.sln.DotSettings.user @@ -0,0 +1,14 @@ + + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded + /home/eelke/.dotnet/dotnet + /home/eelke/.dotnet/sdk/10.0.102/MSBuild.dll + <SessionState ContinuousTestingMode="0" IsActive="True" Name="DecodeTest" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> + <TestAncestor> + <TestId>xUnit::DC887623-8680-4D3B-B23A-D54F7DA91891::net10.0::IdentityShroud.Core.Tests.UnitTest1.DecodeTest</TestId> + <TestId>xUnit::DC887623-8680-4D3B-B23A-D54F7DA91891::net10.0::IdentityShroud.Core.Tests.UnitTest1.CreateTest</TestId> + <TestId>xUnit::DC887623-8680-4D3B-B23A-D54F7DA91891::net10.0::IdentityShroud.Core.Tests.Security.AesGcmHelperTests.EncryptDecryptCycleWorks</TestId> + </TestAncestor> +</SessionState> \ No newline at end of file diff --git a/dotnet-tools.json b/dotnet-tools.json new file mode 100644 index 0000000..a74bf0b --- /dev/null +++ b/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-ef": { + "version": "10.0.2", + "commands": [ + "dotnet-ef" + ], + "rollForward": false + } + } +} \ No newline at end of file