Miscelanious trials
This commit is contained in:
commit
f99c97f392
33 changed files with 881 additions and 0 deletions
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
/packages/
|
||||||
|
riderModule.iml
|
||||||
|
/_ReSharper.Caches/
|
||||||
15
.idea/.idea.IdentityShroud/.idea/.gitignore
generated
vendored
Normal file
15
.idea/.idea.IdentityShroud/.idea/.gitignore
generated
vendored
Normal file
|
|
@ -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/
|
||||||
4
.idea/.idea.IdentityShroud/.idea/encodings.xml
generated
Normal file
4
.idea/.idea.IdentityShroud/.idea/encodings.xml
generated
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
|
||||||
|
</project>
|
||||||
8
.idea/.idea.IdentityShroud/.idea/indexLayout.xml
generated
Normal file
8
.idea/.idea.IdentityShroud/.idea/indexLayout.xml
generated
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="UserContentModel">
|
||||||
|
<attachedFolders />
|
||||||
|
<explicitIncludes />
|
||||||
|
<explicitExcludes />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/.idea.IdentityShroud/.idea/vcs.xml
generated
Normal file
6
.idea/.idea.IdentityShroud/.idea/vcs.xml
generated
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
21
.idea/.idea.IdentityShroud/Docker/compose.generated.override.yml
generated
Normal file
21
.idea/.idea.IdentityShroud/Docker/compose.generated.override.yml
generated
Normal file
|
|
@ -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"
|
||||||
8
IdentityShroud.Api/AppJsonSerializerContext.cs
Normal file
8
IdentityShroud.Api/AppJsonSerializerContext.cs
Normal file
|
|
@ -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
|
||||||
|
{
|
||||||
|
}
|
||||||
24
IdentityShroud.Api/IdentityShroud.Api.csproj
Normal file
24
IdentityShroud.Api/IdentityShroud.Api.csproj
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<InvariantGlobalization>true</InvariantGlobalization>
|
||||||
|
<PublishAot>true</PublishAot>
|
||||||
|
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||||
|
<UserSecretsId>6b8ef434-0577-4a3c-8749-6b547d7787c5</UserSecretsId>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0"/>
|
||||||
|
<PackageReference Include="Serilog" Version="4.3.0" />
|
||||||
|
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
|
||||||
|
<PackageReference Include="Serilog.Expressions" Version="5.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\IdentityShroud.Core\IdentityShroud.Core.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
11
IdentityShroud.Api/IdentityShroud.http
Normal file
11
IdentityShroud.Api/IdentityShroud.http
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
@IdentityShroud_HostAddress = http://localhost:5249
|
||||||
|
|
||||||
|
GET {{IdentityShroud_HostAddress}}/todos/
|
||||||
|
Accept: application/json
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
GET {{IdentityShroud_HostAddress}}/todos/1
|
||||||
|
Accept: application/json
|
||||||
|
|
||||||
|
###
|
||||||
50
IdentityShroud.Api/Program.cs
Normal file
50
IdentityShroud.Api/Program.cs
Normal file
|
|
@ -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<Db>();
|
||||||
|
services.AddOptions<DbConfiguration>().Bind(configuration.GetSection("db"));
|
||||||
|
|
||||||
|
builder.Host.UseSerilog((context, services, configuration) => configuration
|
||||||
|
.Enrich.FromLogContext()
|
||||||
|
//.Enrich.With<UserEnricher>()
|
||||||
|
.ReadFrom.Configuration(context.Configuration));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ConfigureApplication(WebApplication app)
|
||||||
|
{
|
||||||
|
if (app.Environment.IsDevelopment())
|
||||||
|
{
|
||||||
|
app.MapOpenApi();
|
||||||
|
}
|
||||||
|
app.UseSerilogRequestLogging();
|
||||||
|
app.MapRealmEndpoints();
|
||||||
|
// app.UseRouting();
|
||||||
|
// app.MapControllers();
|
||||||
|
}
|
||||||
15
IdentityShroud.Api/Properties/launchSettings.json
Normal file
15
IdentityShroud.Api/Properties/launchSettings.json
Normal file
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
94
IdentityShroud.Api/RealmController.cs
Normal file
94
IdentityShroud.Api/RealmController.cs
Normal file
|
|
@ -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<Results<JsonHttpResult<OpenIdConfiguration>, 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!");
|
||||||
|
// }
|
||||||
|
}
|
||||||
11
IdentityShroud.Api/appsettings.Development.json
Normal file
11
IdentityShroud.Api/appsettings.Development.json
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"Db": {
|
||||||
|
"LogSensitiveData": true
|
||||||
|
},
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
IdentityShroud.Api/appsettings.json
Normal file
9
IdentityShroud.Api/appsettings.json
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*"
|
||||||
|
}
|
||||||
27
IdentityShroud.Core.Tests/IdentityShroud.Core.Tests.csproj
Normal file
27
IdentityShroud.Core.Tests/IdentityShroud.Core.Tests.csproj
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.4"/>
|
||||||
|
<PackageReference Include="jose-jwt" Version="5.2.0" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="10.0.2" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1"/>
|
||||||
|
<PackageReference Include="xunit" Version="2.9.3"/>
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4"/>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Xunit"/>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\IdentityShroud.Core\IdentityShroud.Core.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
21
IdentityShroud.Core.Tests/Security/AesGcmHelperTests.cs
Normal file
21
IdentityShroud.Core.Tests/Security/AesGcmHelperTests.cs
Normal file
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
107
IdentityShroud.Core.Tests/UnitTest1.cs
Normal file
107
IdentityShroud.Core.Tests/UnitTest1.cs
Normal file
|
|
@ -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 = "<RSAKeyValue>...</RSAKeyValue>";
|
||||||
|
// 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<JsonWebTokenHeader>(
|
||||||
|
Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(jwt, 0, firstDot))),
|
||||||
|
Payload = JsonSerializer.Deserialize<JsonWebTokenPayload>(
|
||||||
|
Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(jwt, firstDot + 1, secondDot - (firstDot + 1)))),
|
||||||
|
Signature = WebEncoders.Base64UrlDecode(jwt, secondDot + 1, jwt.Length - (secondDot + 1))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class RsaKeyLoader
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Load RSA private key from PEM format (.NET 5+)
|
||||||
|
/// </summary>
|
||||||
|
public static RSA LoadFromPem(string pemKey)
|
||||||
|
{
|
||||||
|
var rsa = RSA.Create();
|
||||||
|
rsa.ImportFromPem(pemKey);
|
||||||
|
return rsa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Load RSA private key from file
|
||||||
|
/// </summary>
|
||||||
|
public static RSA LoadFromPemFile(string filePath)
|
||||||
|
{
|
||||||
|
string pemContent = System.IO.File.ReadAllText(filePath);
|
||||||
|
return LoadFromPem(pemContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Load RSA private key from PKCS#8 format
|
||||||
|
/// </summary>
|
||||||
|
public static RSA LoadFromPkcs8(byte[] pkcs8Key)
|
||||||
|
{
|
||||||
|
var rsa = RSA.Create();
|
||||||
|
rsa.ImportPkcs8PrivateKey(pkcs8Key, out _);
|
||||||
|
return rsa;
|
||||||
|
}
|
||||||
|
}
|
||||||
6
IdentityShroud.Core/Contracts/ISecretProvider.cs
Normal file
6
IdentityShroud.Core/Contracts/ISecretProvider.cs
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
namespace IdentityShroud.Core.Contracts;
|
||||||
|
|
||||||
|
public interface ISecretProvider
|
||||||
|
{
|
||||||
|
Task<string> GetSecretAsync(string name);
|
||||||
|
}
|
||||||
38
IdentityShroud.Core/Db.cs
Normal file
38
IdentityShroud.Core/Db.cs
Normal file
|
|
@ -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<DbConfiguration> configuration,
|
||||||
|
ILoggerFactory? loggerFactory)
|
||||||
|
: DbContext
|
||||||
|
{
|
||||||
|
public virtual DbSet<Realm> Realms { get; set; }
|
||||||
|
|
||||||
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||||
|
{
|
||||||
|
optionsBuilder.UseNpgsql("<connection string>");
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
21
IdentityShroud.Core/IdentityShroud.Core.csproj
Normal file
21
IdentityShroud.Core/IdentityShroud.Core.csproj
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="EFCore.NamingConventions" Version="10.0.1" />
|
||||||
|
<PackageReference Include="jose-jwt" Version="5.2.0" />
|
||||||
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="Microsoft.AspNetCore.WebUtilities">
|
||||||
|
<HintPath>..\..\..\.nuget\packages\microsoft.aspnetcore.webutilities\10.0.2\lib\net10.0\Microsoft.AspNetCore.WebUtilities.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
34
IdentityShroud.Core/Messages/JsonWebKey.cs
Normal file
34
IdentityShroud.Core/Messages/JsonWebKey.cs
Normal file
|
|
@ -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<string> X509CertificateChain { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("x5t")]
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
public string X509CertificateThumbprint { get; set; }
|
||||||
|
}
|
||||||
9
IdentityShroud.Core/Messages/JsonWebKeySet.cs
Normal file
9
IdentityShroud.Core/Messages/JsonWebKeySet.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace IdentityShroud.Core.Messages;
|
||||||
|
|
||||||
|
public class JsonWebKeySet
|
||||||
|
{
|
||||||
|
[JsonPropertyName("keys")]
|
||||||
|
public List<JsonWebKey> Keys { get; set; } = new List<JsonWebKey>();
|
||||||
|
}
|
||||||
39
IdentityShroud.Core/Messages/JsonWebToken.cs
Normal file
39
IdentityShroud.Core/Messages/JsonWebToken.cs
Normal file
|
|
@ -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; } = [];
|
||||||
|
}
|
||||||
72
IdentityShroud.Core/Messages/OpenIdConfiguration.cs
Normal file
72
IdentityShroud.Core/Messages/OpenIdConfiguration.cs
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace IdentityShroud.Core.Messages;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
|
||||||
|
/// </summary>
|
||||||
|
public class OpenIdConfiguration
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("issuer")]
|
||||||
|
public required string Issuer { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("authorization_endpoint")]
|
||||||
|
public required string AuthorizationEndpoint { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("token_endpoint")]
|
||||||
|
public string? TokenEndpoint { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("userinfo_endpoint")]
|
||||||
|
public string? UserInfoEndpoint { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("jwks_uri")]
|
||||||
|
public required string JwksUri { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("response_types_supported")]
|
||||||
|
public string[] ResponseTypesSupported { get; set; } = [ "code", "id_token", "id_token token"];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// REQUIRED. JSON array containing a list of the Subject Identifier types that this OP supports. Valid types
|
||||||
|
/// include pairwise and public.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("subject_types_supported")]
|
||||||
|
public string[] SubjectTypesSupported { get; set; } = [ "public" ];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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).
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("id_token_signing_alg_values_supported")]
|
||||||
|
public string[] IdTokenSigningAlgValuesSupported { get; set; } = [ "RS256" ];
|
||||||
|
}
|
||||||
7
IdentityShroud.Core/Model/Client.cs
Normal file
7
IdentityShroud.Core/Model/Client.cs
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
namespace IdentityShroud.Core.Model;
|
||||||
|
|
||||||
|
public class Client
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
}
|
||||||
10
IdentityShroud.Core/Model/Realm.cs
Normal file
10
IdentityShroud.Core/Model/Realm.cs
Normal file
|
|
@ -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<Client> Clients { get; set; } = [];
|
||||||
|
public byte[] PrivateKey { get; set; }
|
||||||
|
}
|
||||||
64
IdentityShroud.Core/Security/AesGcmHelper.cs
Normal file
64
IdentityShroud.Core/Security/AesGcmHelper.cs
Normal file
|
|
@ -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<byte> nonce = new(payload, 0, nonceSize);
|
||||||
|
ReadOnlySpan<byte> ciphertext = new(payload, nonceSize, payload.Length - nonceSize - tagSize);
|
||||||
|
ReadOnlySpan<byte> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
38
IdentityShroud.Core/Security/JwtSignatureGenerator.cs
Normal file
38
IdentityShroud.Core/Security/JwtSignatureGenerator.cs
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using Microsoft.AspNetCore.WebUtilities;
|
||||||
|
|
||||||
|
namespace IdentityShroud.Core;
|
||||||
|
|
||||||
|
public class JwtSignatureGenerator
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Generates a JWT signature using RS256 algorithm
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="headerBase64Url">Base64Url encoded header</param>
|
||||||
|
/// <param name="payloadBase64Url">Base64Url encoded payload</param>
|
||||||
|
/// <param name="privateKey">RSA private key (PEM format or RSA parameters)</param>
|
||||||
|
/// <returns>Base64Url encoded signature</returns>
|
||||||
|
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}";
|
||||||
|
}
|
||||||
|
}
|
||||||
21
IdentityShroud.Migrations/DesignTimeDbFactory.cs
Normal file
21
IdentityShroud.Migrations/DesignTimeDbFactory.cs
Normal file
|
|
@ -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<Db>
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
20
IdentityShroud.Migrations/IdentityShroud.Migrations.csproj
Normal file
20
IdentityShroud.Migrations/IdentityShroud.Migrations.csproj
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.2">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\IdentityShroud.Core\IdentityShroud.Core.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
39
IdentityShroud.sln
Normal file
39
IdentityShroud.sln
Normal file
|
|
@ -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
|
||||||
14
IdentityShroud.sln.DotSettings.user
Normal file
14
IdentityShroud.sln.DotSettings.user
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHealthCheckEndpointRouteBuilderExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F6d0f079e13da4e98881aa3e6e169c6d34f08_003F0e_003Fc2b30661_003FHealthCheckEndpointRouteBuilderExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ANamingConventionsExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FSourcesCache_003Feacd26cff49d864d97bf44d3424fd383a26620b1d0c43fb1d6f115da85c655_003FNamingConventionsExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AOkOfT_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fe2a19de442f561af862af2dcad0852b7e62707a5cf194d266d1656f92bbb6d2_003FOkOfT_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ATypedResults_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fcea118513a410f660e578fe32bed95cf86457dd135e4b4632ca91eb4f7b_003FTypedResults_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/Environment/Hierarchy/Build/BuildTool/DotNetCliExePath/@EntryValue">/home/eelke/.dotnet/dotnet</s:String>
|
||||||
|
<s:String x:Key="/Default/Environment/Hierarchy/Build/BuildTool/CustomBuildToolPath/@EntryValue">/home/eelke/.dotnet/sdk/10.0.102/MSBuild.dll</s:String>
|
||||||
|
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=7283fa6f_002Dab5a_002D49e4_002Db89b_002D4cdb1b0ba2b3/@EntryIndexedValue"><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></s:String></wpf:ResourceDictionary>
|
||||||
13
dotnet-tools.json
Normal file
13
dotnet-tools.json
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"isRoot": true,
|
||||||
|
"tools": {
|
||||||
|
"dotnet-ef": {
|
||||||
|
"version": "10.0.2",
|
||||||
|
"commands": [
|
||||||
|
"dotnet-ef"
|
||||||
|
],
|
||||||
|
"rollForward": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue