Browse Source

Add CIMD sandbox demonstrator server (Phase 0)

Minimal ASP.NET Core server for testing CIMD support. Seeds a
pre-registered public client (test-client) and test user, supports
authorization code + PKCE and password grant flows. This serves as
the baseline to verify token issuance before adding CIMD handling.
pull/2416/head
Thor Arne Johansen 7 days ago
parent
commit
9773c098e8
  1. 104
      CLAUDE.md
  2. 1
      OpenIddict.slnx
  3. 219
      sandbox/OpenIddict.Sandbox.AspNetCore.CimdServer/AuthorizationController.cs
  4. 12
      sandbox/OpenIddict.Sandbox.AspNetCore.CimdServer/Models/ApplicationDbContext.cs
  5. 5
      sandbox/OpenIddict.Sandbox.AspNetCore.CimdServer/Models/ApplicationUser.cs
  6. 22
      sandbox/OpenIddict.Sandbox.AspNetCore.CimdServer/OpenIddict.Sandbox.AspNetCore.CimdServer.csproj
  7. 64
      sandbox/OpenIddict.Sandbox.AspNetCore.CimdServer/Program.cs
  8. 12
      sandbox/OpenIddict.Sandbox.AspNetCore.CimdServer/Properties/launchSettings.json
  9. 80
      sandbox/OpenIddict.Sandbox.AspNetCore.CimdServer/Worker.cs

104
CLAUDE.md

@ -0,0 +1,104 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
OpenIddict is an open-source framework for building OAuth 2.0/OpenID Connect servers and clients in .NET. It provides a modular architecture with pluggable stores (EF Core, EF 6.x, MongoDB) and host integrations (ASP.NET Core, OWIN).
## Build Commands
The project uses Microsoft.DotNet.Arcade.Sdk with .NET SDK 10.0.
```bash
# Build (Windows)
Build.cmd
# Build (Unix/macOS)
./build.sh
# Full CI build with tests
# Windows:
eng\common\Build.cmd -restore -build -test
# Unix:
./eng/common/build.sh -restore -build -test
# Build specific project
dotnet build src/OpenIddict.Core/OpenIddict.Core.csproj
# Run all tests
dotnet test
# Run a single test project
dotnet test test/OpenIddict.Core.Tests/OpenIddict.Core.Tests.csproj
# Run a single test by name
dotnet test test/OpenIddict.Core.Tests/OpenIddict.Core.Tests.csproj --filter "FullyQualifiedName~MyTestMethod"
# Full CI build (restore, build, test, sign, pack)
eng\common\Build.cmd -configuration Release -ci -prepareMachine -restore -build -test -sign -pack
```
## Multi-Targeting
Projects target many frameworks simultaneously: net462, net472, net48, netstandard2.0, netstandard2.1, net8.0, net9.0, net10.0, plus mobile platforms (Android, iOS, macOS, Mac Catalyst, Windows) when workloads are available.
Conditional compilation uses `SUPPORTS_*` defines (e.g., `SUPPORTS_KEY_DERIVATION_WITH_SPECIFIED_HASH_ALGORITHM`, `SUPPORTS_CERTIFICATE_LOADER`) set in `Directory.Build.targets`. Use `#if` directives for platform-specific code paths.
## Architecture
### Core Abstraction Layers
1. **OpenIddict.Abstractions** — Interfaces, constants, descriptors, store contracts, and primitive types. Everything depends on this.
2. **OpenIddict.Core** — Generic managers (`OpenIddictApplicationManager<T>`, `OpenIddictAuthorizationManager<T>`, `OpenIddictScopeManager<T>`, `OpenIddictTokenManager<T>`) and caches. Managers coordinate stores, caches, validation, and business logic.
3. **OpenIddict.Server / .Client / .Validation** — Protocol implementations using an event-driven handler/filter/dispatcher pattern.
### Store Pattern
Store interfaces (`IOpenIddictApplicationStore<T>`, etc.) abstract persistence. Implementations:
- `OpenIddict.EntityFrameworkCore` — EF Core stores
- `OpenIddict.EntityFramework` — EF 6.x stores
- `OpenIddict.MongoDb` — MongoDB stores
- Corresponding `.Models` projects hold entity classes
### Host Integration Pattern
Each protocol component (Server, Client, Validation) has host-specific projects:
- `.AspNetCore` — ASP.NET Core middleware integration
- `.Owin` — OWIN/Katana integration for ASP.NET 4.x
- `.DataProtection` — ASP.NET Core Data Protection token formats
### Handler/Filter Pattern
Request processing uses event handlers guarded by filters. Handlers are registered via `IOpenIddictServerHandlers`, and filters (e.g., `RequireAccessTokenGenerated`, `RequireClientIdParameter`) control execution flow.
### DI Registration
Builder pattern via `OpenIddictBuilder` with extension methods in `Microsoft.Extensions.DependencyInjection` namespace. Uses `TryAddScoped`/`TryAddSingleton` for idempotent registration.
## Code Conventions
- C# 14, nullable reference types enabled, implicit usings enabled (`Directory.Build.props`)
- `TreatWarningsAsErrors: true`
- File-scoped namespaces throughout
- `ValueTask` for async manager methods; `CancellationToken` on all async APIs
- `SR.GetResourceString()` for localized exception messages (resource strings in Abstractions)
- Strong-name signed assemblies (key at `eng/key.snk`)
- CRLF line endings, 4-space indentation (enforced via `.editorconfig`)
## Testing
- xUnit with Moq
- Test projects mirror source projects under `/test/` (e.g., `OpenIddict.Core.Tests`)
- Integration tests in `*.IntegrationTests` projects
- `[Fact]` for deterministic tests, `[Theory]` + `[InlineData]` for parameterized tests
- CI runs tests on Windows, Ubuntu, and macOS
## Key Directories
- `/eng/` — Arcade build infrastructure, signing key
- `/gen/` — Code generators (Client WebIntegration provider generator)
- `/src/` — Source libraries (~30 projects)
- `/test/` — Test projects (~18 projects)
- `/sandbox/` — Sample/reference applications
- `/shared/OpenIddict.Extensions/` — Shared helper utilities and polyfills

1
OpenIddict.slnx

@ -35,6 +35,7 @@
<Folder Name="/sandbox/">
<Project Path="sandbox/OpenIddict.Sandbox.AspNet.Client/OpenIddict.Sandbox.AspNet.Client.csproj" />
<Project Path="sandbox/OpenIddict.Sandbox.AspNet.Server/OpenIddict.Sandbox.AspNet.Server.csproj" />
<Project Path="sandbox/OpenIddict.Sandbox.AspNetCore.CimdServer/OpenIddict.Sandbox.AspNetCore.CimdServer.csproj" />
<Project Path="sandbox/OpenIddict.Sandbox.AspNetCore.Client/OpenIddict.Sandbox.AspNetCore.Client.csproj" />
<Project Path="sandbox/OpenIddict.Sandbox.AspNetCore.Server/OpenIddict.Sandbox.AspNetCore.Server.csproj" />
<Project Path="sandbox/OpenIddict.Sandbox.Console.Client/OpenIddict.Sandbox.Console.Client.csproj" />

219
sandbox/OpenIddict.Sandbox.AspNetCore.CimdServer/AuthorizationController.cs

@ -0,0 +1,219 @@
using System.Security.Claims;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Abstractions;
using OpenIddict.Sandbox.AspNetCore.CimdServer.Models;
using OpenIddict.Server.AspNetCore;
using static OpenIddict.Abstractions.OpenIddictConstants;
namespace OpenIddict.Sandbox.AspNetCore.CimdServer;
public class AuthorizationController : Controller
{
private readonly IOpenIddictApplicationManager _applicationManager;
private readonly IOpenIddictAuthorizationManager _authorizationManager;
private readonly IOpenIddictScopeManager _scopeManager;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly UserManager<ApplicationUser> _userManager;
public AuthorizationController(
IOpenIddictApplicationManager applicationManager,
IOpenIddictAuthorizationManager authorizationManager,
IOpenIddictScopeManager scopeManager,
SignInManager<ApplicationUser> signInManager,
UserManager<ApplicationUser> userManager)
{
_applicationManager = applicationManager;
_authorizationManager = authorizationManager;
_scopeManager = scopeManager;
_signInManager = signInManager;
_userManager = userManager;
}
[HttpGet("~/connect/authorize")]
[HttpPost("~/connect/authorize")]
[IgnoreAntiforgeryToken]
public async Task<IActionResult> Authorize()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
// Try to retrieve the user principal stored in the authentication cookie.
var result = await HttpContext.AuthenticateAsync();
if (result is not { Succeeded: true })
{
// If the user is not logged in, redirect to login.
// For this demo, we auto-sign in the test user to simplify testing.
var user = await _userManager.FindByNameAsync("testuser");
if (user is not null)
{
await _signInManager.SignInAsync(user, isPersistent: false);
return Challenge();
}
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string?>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.LoginRequired,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is not logged in."
}));
}
var userEntity = await _userManager.GetUserAsync(result.Principal) ??
throw new InvalidOperationException("The user details cannot be retrieved.");
var application = await _applicationManager.FindByClientIdAsync(request.ClientId!) ??
throw new InvalidOperationException("Details concerning the calling client application cannot be found.");
// Auto-approve consent for this demonstrator.
var identity = new ClaimsIdentity(
authenticationType: TokenValidationParameters.DefaultAuthenticationType,
nameType: Claims.Name,
roleType: Claims.Role);
identity.SetClaim(Claims.Subject, await _userManager.GetUserIdAsync(userEntity))
.SetClaim(Claims.Email, await _userManager.GetEmailAsync(userEntity))
.SetClaim(Claims.Name, await _userManager.GetUserNameAsync(userEntity));
identity.SetScopes(request.GetScopes());
identity.SetResources(await _scopeManager.ListResourcesAsync(identity.GetScopes()).ToListAsync());
var authorization = await _authorizationManager.CreateAsync(
identity: identity,
subject: await _userManager.GetUserIdAsync(userEntity),
client: (await _applicationManager.GetIdAsync(application))!,
type: AuthorizationTypes.Permanent,
scopes: identity.GetScopes());
identity.SetAuthorizationId(await _authorizationManager.GetIdAsync(authorization));
identity.SetDestinations(GetDestinations);
return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
[HttpPost("~/connect/token")]
[IgnoreAntiforgeryToken]
[Produces("application/json")]
public async Task<IActionResult> Exchange()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
if (request.IsPasswordGrantType())
{
var user = await _userManager.FindByNameAsync(request.Username!);
if (user is null)
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string?>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The username/password couple is invalid."
}));
}
var passwordResult = await _signInManager.CheckPasswordSignInAsync(user, request.Password!, lockoutOnFailure: false);
if (!passwordResult.Succeeded)
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string?>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The username/password couple is invalid."
}));
}
var identity = new ClaimsIdentity(
authenticationType: TokenValidationParameters.DefaultAuthenticationType,
nameType: Claims.Name,
roleType: Claims.Role);
identity.SetClaim(Claims.Subject, await _userManager.GetUserIdAsync(user))
.SetClaim(Claims.Email, await _userManager.GetEmailAsync(user))
.SetClaim(Claims.Name, await _userManager.GetUserNameAsync(user));
identity.SetScopes(request.GetScopes());
identity.SetResources(await _scopeManager.ListResourcesAsync(identity.GetScopes()).ToListAsync());
identity.SetDestinations(GetDestinations);
return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
if (request.IsAuthorizationCodeGrantType() || request.IsRefreshTokenGrantType())
{
var result = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
var user = await _userManager.FindByIdAsync(result.Principal!.GetClaim(Claims.Subject)!);
if (user is null)
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string?>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The token is no longer valid."
}));
}
if (!await _signInManager.CanSignInAsync(user))
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string?>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is no longer allowed to sign in."
}));
}
var identity = new ClaimsIdentity(result.Principal!.Claims,
authenticationType: TokenValidationParameters.DefaultAuthenticationType,
nameType: Claims.Name,
roleType: Claims.Role);
identity.SetClaim(Claims.Subject, await _userManager.GetUserIdAsync(user))
.SetClaim(Claims.Email, await _userManager.GetEmailAsync(user))
.SetClaim(Claims.Name, await _userManager.GetUserNameAsync(user));
identity.SetDestinations(GetDestinations);
return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
throw new InvalidOperationException("The specified grant type is not supported.");
}
private static IEnumerable<string> GetDestinations(Claim claim)
{
switch (claim.Type)
{
case Claims.Name:
yield return Destinations.AccessToken;
if (claim.Subject!.HasScope(Scopes.Profile))
yield return Destinations.IdentityToken;
yield break;
case Claims.Email:
yield return Destinations.AccessToken;
if (claim.Subject!.HasScope(Scopes.Email))
yield return Destinations.IdentityToken;
yield break;
case "AspNet.Identity.SecurityStamp": yield break;
default:
yield return Destinations.AccessToken;
yield break;
}
}
}

12
sandbox/OpenIddict.Sandbox.AspNetCore.CimdServer/Models/ApplicationDbContext.cs

@ -0,0 +1,12 @@
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
namespace OpenIddict.Sandbox.AspNetCore.CimdServer.Models;
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
public ApplicationDbContext(DbContextOptions options)
: base(options)
{
}
}

5
sandbox/OpenIddict.Sandbox.AspNetCore.CimdServer/Models/ApplicationUser.cs

@ -0,0 +1,5 @@
using Microsoft.AspNetCore.Identity;
namespace OpenIddict.Sandbox.AspNetCore.CimdServer.Models;
public class ApplicationUser : IdentityUser { }

22
sandbox/OpenIddict.Sandbox.AspNetCore.CimdServer/OpenIddict.Sandbox.AspNetCore.CimdServer.csproj

@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFrameworks>net10.0</TargetFrameworks>
<TypeScriptEnabled>false</TypeScriptEnabled>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\OpenIddict.Core\OpenIddict.Core.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.Server.AspNetCore\OpenIddict.Server.AspNetCore.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.Server.DataProtection\OpenIddict.Server.DataProtection.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.Validation.AspNetCore\OpenIddict.Validation.AspNetCore.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.Validation.ServerIntegration\OpenIddict.Validation.ServerIntegration.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.EntityFrameworkCore\OpenIddict.EntityFrameworkCore.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
</ItemGroup>
</Project>

64
sandbox/OpenIddict.Sandbox.AspNetCore.CimdServer/Program.cs

@ -0,0 +1,64 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using OpenIddict.Sandbox.AspNetCore.CimdServer;
using OpenIddict.Sandbox.AspNetCore.CimdServer.Models;
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseUrls("https://localhost:7295");
builder.Services.AddControllers();
builder.Services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseSqlite($"Filename={Path.Combine(Path.GetTempPath(), "openiddict-sandbox-aspnetcore-cimdserver.sqlite3")}");
options.UseOpenIddict();
});
builder.Services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
builder.Services.AddOpenIddict()
.AddCore(options =>
{
options.UseEntityFrameworkCore()
.UseDbContext<ApplicationDbContext>();
})
.AddServer(options =>
{
options.SetAuthorizationEndpointUris("connect/authorize")
.SetTokenEndpointUris("connect/token");
options.AllowAuthorizationCodeFlow()
.AllowPasswordFlow()
.AllowRefreshTokenFlow();
options.RequireProofKeyForCodeExchange();
options.RegisterScopes("openid", "profile", "email");
options.AddDevelopmentEncryptionCertificate()
.AddDevelopmentSigningCertificate();
options.UseAspNetCore()
.EnableAuthorizationEndpointPassthrough()
.EnableTokenEndpointPassthrough();
})
.AddValidation(options =>
{
options.UseLocalServer();
options.UseAspNetCore();
});
builder.Services.AddHostedService<Worker>();
var app = builder.Build();
app.UseDeveloperExceptionPage();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();

12
sandbox/OpenIddict.Sandbox.AspNetCore.CimdServer/Properties/launchSettings.json

@ -0,0 +1,12 @@
{
"profiles": {
"OpenIddict.Sandbox.AspNetCore.CimdServer": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:53769;http://localhost:53770"
}
}
}

80
sandbox/OpenIddict.Sandbox.AspNetCore.CimdServer/Worker.cs

@ -0,0 +1,80 @@
using Microsoft.AspNetCore.Identity;
using OpenIddict.Abstractions;
using OpenIddict.Sandbox.AspNetCore.CimdServer.Models;
using static OpenIddict.Abstractions.OpenIddictConstants;
namespace OpenIddict.Sandbox.AspNetCore.CimdServer;
public class Worker : IHostedService
{
private readonly IServiceProvider _provider;
public Worker(IServiceProvider provider)
=> _provider = provider;
public async Task StartAsync(CancellationToken cancellationToken)
{
await using var scope = _provider.CreateAsyncScope();
var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
await context.Database.EnsureCreatedAsync(cancellationToken);
await SeedUsersAsync(scope.ServiceProvider);
await SeedClientsAsync(scope.ServiceProvider);
}
private static async Task SeedUsersAsync(IServiceProvider provider)
{
var userManager = provider.GetRequiredService<UserManager<ApplicationUser>>();
if (await userManager.FindByNameAsync("testuser") is null)
{
var user = new ApplicationUser
{
UserName = "testuser",
Email = "testuser@example.com"
};
await userManager.CreateAsync(user, "Pass123$");
}
}
private static async Task SeedClientsAsync(IServiceProvider provider)
{
var manager = provider.GetRequiredService<IOpenIddictApplicationManager>();
// Pre-registered test client for baseline verification.
if (await manager.FindByClientIdAsync("test-client") is null)
{
await manager.CreateAsync(new OpenIddictApplicationDescriptor
{
ApplicationType = ApplicationTypes.Native,
ClientId = "test-client",
ClientType = ClientTypes.Public,
ConsentType = ConsentTypes.Systematic,
DisplayName = "Test client (pre-registered)",
RedirectUris =
{
new Uri("http://localhost/callback")
},
Permissions =
{
Permissions.Endpoints.Authorization,
Permissions.Endpoints.Token,
Permissions.GrantTypes.AuthorizationCode,
Permissions.GrantTypes.Password,
Permissions.GrantTypes.RefreshToken,
Permissions.ResponseTypes.Code,
Permissions.Scopes.Email,
Permissions.Scopes.Profile
},
Requirements =
{
Requirements.Features.ProofKeyForCodeExchange
}
});
}
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
Loading…
Cancel
Save