Merge pull request #2271 from DuendeSoftware/ev/bff/update-bff-3.1.x-to-main

Ensure BFF V3.1.x release branch is up to date with main.
This commit is contained in:
Erwin van der Valk 2025-11-18 15:58:10 +01:00 committed by GitHub
commit fc63bc0ef3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
57 changed files with 1240 additions and 282 deletions

View file

@ -95,7 +95,7 @@ void GenerateCiWorkflow(Product product)
.Job(BuildJobId)
.RunEitherOnBranchOrAsPR()
.Name("Build and test (unit)")
.RunsOn(GitHubHostedRunners.UbuntuLatest)
.RunsOn("large", ["ubuntu-latest-x64-16core"])
.Defaults().Run("bash", product.Name)
.Job;

View file

@ -18,7 +18,7 @@ public static class StepExtensions
job.Step()
.Name("Setup .NET")
.ActionsSetupDotNet("3e891b0cb619bf60e2c25674b222b8940e2c1c25", ["8.0.x", "9.0.203", "10.0.100-rc.2.25502.107"]);
.ActionsSetupDotNet("3e891b0cb619bf60e2c25674b222b8940e2c1c25", ["8.0.x", "9.0.203", "10.0.100"]);
// v4.1.0
}

View file

@ -53,7 +53,7 @@ jobs:
dotnet-version: |-
8.0.x
9.0.203
10.0.100-rc.2.25502.107
10.0.100
- name: Restore
run: dotnet restore aspnetcore-authentication-jwtbearer.slnf
- name: Verify Formatting
@ -61,7 +61,9 @@ jobs:
build:
name: Build and test (unit)
if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) || (github.event_name == 'push') || (github.event_name == 'workflow_dispatch')
runs-on: ubuntu-latest
runs-on:
group: large
labels: [ubuntu-latest-x64-16core]
permissions:
actions: read
checks: write
@ -85,7 +87,7 @@ jobs:
dotnet-version: |-
8.0.x
9.0.203
10.0.100-rc.2.25502.107
10.0.100
- name: Restore
run: dotnet restore aspnetcore-authentication-jwtbearer.slnf
- name: Build
@ -174,7 +176,7 @@ jobs:
dotnet-version: |-
8.0.x
9.0.203
10.0.100-rc.2.25502.107
10.0.100
- name: Tool restore
run: dotnet tool restore
- name: Pack aspnetcore-authentication-jwtbearer.slnf

View file

@ -68,7 +68,7 @@ jobs:
dotnet-version: |-
8.0.x
9.0.203
10.0.100-rc.2.25502.107
10.0.100
- name: Pack aspnetcore-authentication-jwtbearer.slnf
run: dotnet pack -c Release aspnetcore-authentication-jwtbearer.slnf -o artifacts
- name: Tool restore
@ -110,7 +110,7 @@ jobs:
dotnet-version: |-
8.0.x
9.0.203
10.0.100-rc.2.25502.107
10.0.100
- name: List files
run: tree
shell: bash

View file

@ -53,7 +53,7 @@ jobs:
dotnet-version: |-
8.0.x
9.0.203
10.0.100-rc.2.25502.107
10.0.100
- name: Restore
run: dotnet restore bff.slnf
- name: Verify Formatting
@ -61,7 +61,9 @@ jobs:
build:
name: Build and test (unit)
if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) || (github.event_name == 'push') || (github.event_name == 'workflow_dispatch')
runs-on: ubuntu-latest
runs-on:
group: large
labels: [ubuntu-latest-x64-16core]
permissions:
actions: read
checks: write
@ -85,7 +87,7 @@ jobs:
dotnet-version: |-
8.0.x
9.0.203
10.0.100-rc.2.25502.107
10.0.100
- name: Restore
run: dotnet restore bff.slnf
- name: Build
@ -134,7 +136,7 @@ jobs:
dotnet-version: |-
8.0.x
9.0.203
10.0.100-rc.2.25502.107
10.0.100
- name: Restore
run: dotnet restore bff.slnf
- name: Build
@ -221,7 +223,7 @@ jobs:
dotnet-version: |-
8.0.x
9.0.203
10.0.100-rc.2.25502.107
10.0.100
- name: Tool restore
run: dotnet tool restore
- name: Pack bff.slnf

View file

@ -68,7 +68,7 @@ jobs:
dotnet-version: |-
8.0.x
9.0.203
10.0.100-rc.2.25502.107
10.0.100
- name: Pack bff.slnf
run: dotnet pack -c Release bff.slnf -o artifacts
- name: Tool restore
@ -110,7 +110,7 @@ jobs:
dotnet-version: |-
8.0.x
9.0.203
10.0.100-rc.2.25502.107
10.0.100
- name: List files
run: tree
shell: bash

View file

@ -53,7 +53,7 @@ jobs:
dotnet-version: |-
8.0.x
9.0.203
10.0.100-rc.2.25502.107
10.0.100
- name: Restore
run: dotnet restore docs-mcp.slnf
- name: Verify Formatting
@ -61,7 +61,9 @@ jobs:
build:
name: Build and test (unit)
if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) || (github.event_name == 'push') || (github.event_name == 'workflow_dispatch')
runs-on: ubuntu-latest
runs-on:
group: large
labels: [ubuntu-latest-x64-16core]
permissions:
actions: read
checks: write
@ -85,7 +87,7 @@ jobs:
dotnet-version: |-
8.0.x
9.0.203
10.0.100-rc.2.25502.107
10.0.100
- name: Restore
run: dotnet restore docs-mcp.slnf
- name: Build
@ -160,7 +162,7 @@ jobs:
dotnet-version: |-
8.0.x
9.0.203
10.0.100-rc.2.25502.107
10.0.100
- name: Tool restore
run: dotnet tool restore
- name: Pack docs-mcp.slnf

View file

@ -68,7 +68,7 @@ jobs:
dotnet-version: |-
8.0.x
9.0.203
10.0.100-rc.2.25502.107
10.0.100
- name: Pack docs-mcp.slnf
run: dotnet pack -c Release docs-mcp.slnf -o artifacts
- name: Tool restore
@ -110,7 +110,7 @@ jobs:
dotnet-version: |-
8.0.x
9.0.203
10.0.100-rc.2.25502.107
10.0.100
- name: List files
run: tree
shell: bash

View file

@ -53,7 +53,7 @@ jobs:
dotnet-version: |-
8.0.x
9.0.203
10.0.100-rc.2.25502.107
10.0.100
- name: Restore
run: dotnet restore identity-server.slnf
- name: Verify Formatting
@ -61,7 +61,9 @@ jobs:
build:
name: Build and test (unit)
if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) || (github.event_name == 'push') || (github.event_name == 'workflow_dispatch')
runs-on: ubuntu-latest
runs-on:
group: large
labels: [ubuntu-latest-x64-16core]
permissions:
actions: read
checks: write
@ -85,7 +87,7 @@ jobs:
dotnet-version: |-
8.0.x
9.0.203
10.0.100-rc.2.25502.107
10.0.100
- name: Restore
run: dotnet restore identity-server.slnf
- name: Build
@ -148,7 +150,7 @@ jobs:
dotnet-version: |-
8.0.x
9.0.203
10.0.100-rc.2.25502.107
10.0.100
- name: Restore
run: dotnet restore identity-server.slnf
- name: Build
@ -235,7 +237,7 @@ jobs:
dotnet-version: |-
8.0.x
9.0.203
10.0.100-rc.2.25502.107
10.0.100
- name: Tool restore
run: dotnet tool restore
- name: Pack identity-server.slnf

View file

@ -68,7 +68,7 @@ jobs:
dotnet-version: |-
8.0.x
9.0.203
10.0.100-rc.2.25502.107
10.0.100
- name: Pack identity-server.slnf
run: dotnet pack -c Release identity-server.slnf -o artifacts
- name: Tool restore
@ -110,7 +110,7 @@ jobs:
dotnet-version: |-
8.0.x
9.0.203
10.0.100-rc.2.25502.107
10.0.100
- name: List files
run: tree
shell: bash

View file

@ -46,7 +46,7 @@ jobs:
dotnet-version: |-
8.0.x
9.0.203
10.0.100-rc.2.25502.107
10.0.100
- name: Checkout target branch
if: github.event.inputs.branch != 'main'
run: git checkout ${{ github.event.inputs.branch }}
@ -110,7 +110,7 @@ jobs:
dotnet-version: |-
8.0.x
9.0.203
10.0.100-rc.2.25502.107
10.0.100
- name: List files
run: tree
shell: bash

View file

@ -1,59 +1,56 @@
<!-- We have agreed to explicitly target where possible version 10 of the package(s)
that supports the target frameworks our products target (8, 9, 10) -->
<Project>
<PropertyGroup Condition=" '$(TargetFramework)' == 'net8.0'">
<FrameworkVersion>8.0.1</FrameworkVersion>
<!-- Package Versions Note:
These versions are here because they don't explicitly have a version 10 of the package that supports .NET 8, 9 and 10 -->
<EFCoreVersion>9.0.9</EFCoreVersion>
<IdentityEFCoreVersion>8.0.20</IdentityEFCoreVersion>
<RuntimeCompilationVersion>8.0.20</RuntimeCompilationVersion>
<MvcRazorRuntimeCompilationVersion>8.0.20</MvcRazorRuntimeCompilationVersion>
<AuthenticationCertificateVersion>8.0.16</AuthenticationCertificateVersion>
<!-- /End Note -->
<IdentityModelVersion>7.1.2</IdentityModelVersion>
<CachingMemoryVersion>9.0.3</CachingMemoryVersion>
<SystemTextJsonVersion>9.0.9</SystemTextJsonVersion>
<LoggingAbstractionsVersion>9.0.9</LoggingAbstractionsVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(TargetFramework)' == 'net9.0'">
<FrameworkVersion>9.0.3</FrameworkVersion>
<EFCoreVersion>9.0.9</EFCoreVersion>
<IdentityEFCoreVersion>9.0.3</IdentityEFCoreVersion>
<RuntimeCompilationVersion>9.0.3</RuntimeCompilationVersion>
<MvcRazorRuntimeCompilationVersion>9.0.3</MvcRazorRuntimeCompilationVersion>
<AuthenticationCertificateVersion>9.0.3</AuthenticationCertificateVersion>
<IdentityModelVersion>8.0.1</IdentityModelVersion>
<CachingMemoryVersion>9.0.3</CachingMemoryVersion>
<SystemTextJsonVersion>9.0.9</SystemTextJsonVersion>
<LoggingAbstractionsVersion>9.0.9</LoggingAbstractionsVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(TargetFramework)' == 'net10.0'">
<FrameworkVersion>10.0.0-rc.2.25502.107</FrameworkVersion>
<EFCoreVersion>10.0.0-rc.2.25502.107</EFCoreVersion>
<IdentityEFCoreVersion>10.0.0-rc.2.25502.107</IdentityEFCoreVersion>
<RuntimeCompilationVersion>10.0.0-rc.2.25502.107</RuntimeCompilationVersion>
<AuthenticationCertificateVersion>10.0.0-rc.2.25502.107</AuthenticationCertificateVersion>
<FrameworkVersion>10.0.0</FrameworkVersion>
<EFCoreVersion>10.0.0</EFCoreVersion>
<IdentityEFCoreVersion>10.0.0</IdentityEFCoreVersion>
<MvcRazorRuntimeCompilationVersion>10.0.0</MvcRazorRuntimeCompilationVersion>
<AuthenticationCertificateVersion>10.0.0</AuthenticationCertificateVersion>
<IdentityModelVersion>8.0.1</IdentityModelVersion>
<CachingMemoryVersion>10.0.0-rc.2.25502.107</CachingMemoryVersion>
<SystemTextJsonVersion>10.0.0-rc.2.25502.107</SystemTextJsonVersion>
<LoggingAbstractionsVersion>10.0.0-rc.2.25502.107</LoggingAbstractionsVersion>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="AngleSharp" Version="1.1.2" />
<PackageVersion Include="Aspire.Hosting.AppHost" Version="9.5.0" />
<PackageVersion Include="Aspire.Hosting.Testing" Version="9.5.0" />
<PackageVersion Include="Aspire.Hosting.SqlServer" Version="9.5.0" />
<PackageVersion Include="Aspire.Hosting.AppHost" Version="13.0.0" />
<PackageVersion Include="Aspire.Hosting.Testing" Version="13.0.0" />
<PackageVersion Include="Aspire.Hosting.SqlServer" Version="13.0.0" />
<PackageVersion Include="BenchmarkDotNet" Version="0.15.0" />
<PackageVersion Include="BullsEye" Version="5.0.0" />
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
<!-- Added aspire transitive package to resolve package vulnerability -->
<PackageVersion Include="KubernetesClient" Version="17.0.14" />
<PackageVersion Include="Duende.AccessTokenManagement" Version="[3.3.0-preview.1, 4.0.0)" Condition="'$(IsBffProject)' == 'true'" />
<PackageVersion Include="Duende.AccessTokenManagement" Version="4.1.0-preview.2" Condition="'$(IsBffProject)' != 'true'" />
<PackageVersion Include="Duende.AccessTokenManagement.OpenIdConnect" Version="[3.3.0-preview.1, 4.0.0)" Condition="'$(IsBffProject)' == 'true'" />
<PackageVersion Include="Duende.AccessTokenManagement.OpenIdConnect" Version="4.1.0-preview.2" Condition="'$(IsBffProject)' != 'true'" />
<PackageVersion Include="Duende.AccessTokenManagement" Version="[3.3.0-rc.1, 4.0.0)" Condition="'$(IsBffProject)' == 'true'" />
<PackageVersion Include="Duende.AccessTokenManagement" Version="4.1.0-rc.1" Condition="'$(IsBffProject)' != 'true'" />
<PackageVersion Include="Duende.AccessTokenManagement.OpenIdConnect" Version="[3.3.0-rc.1, 4.0.0)" Condition="'$(IsBffProject)' == 'true'" />
<PackageVersion Include="Duende.AccessTokenManagement.OpenIdConnect" Version="4.1.0-rc.1" Condition="'$(IsBffProject)' != 'true'" />
<PackageVersion Include="Duende.AspNetCore.Authentication.JwtBearer" Version="0.1.3" />
<PackageVersion Include="Duende.IdentityModel" Version="8.0.0-preview.1" />
<PackageVersion Include="Duende.IdentityModel.OidcClient" Version="7.0.0-preview.2" />
<PackageVersion Include="Duende.IdentityModel" Version="8.0.0-rc.1" />
<PackageVersion Include="Duende.IdentityModel.OidcClient" Version="7.0.0-rc.1" />
<PackageVersion Include="Duende.IdentityServer" Version="7.4.0-preview.2" />
<PackageVersion Include="Duende.Private.Licensing" Version="1.0.0" />
<PackageVersion Include="IdentityModel.AspNetCore.OAuth2Introspection" Version="6.2.0" />
<PackageVersion Include="Markdig" Version="0.42.0" />
<PackageVersion Include="Meziantou.Extensions.Logging.Xunit" Version="1.0.8" />
<!-- Review Packages -->
<PackageVersion Include="Microsoft.AspNetCore.Authentication.Certificate" Version="$(AuthenticationCertificateVersion)" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="$(FrameworkVersion)" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="$(FrameworkVersion)" />
@ -61,10 +58,9 @@
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="$(FrameworkVersion)" />
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="$(FrameworkVersion)" />
<PackageVersion Include="Microsoft.AspNetCore.Components.Authorization" Version="$(FrameworkVersion)" />
<PackageVersion Include="Microsoft.AspNetCore.DataProtection.Abstractions" Version="$(FrameworkVersion)" />
<PackageVersion Include="Microsoft.AspNetCore.Identity" Version="$(FrameworkVersion)" />
<PackageVersion Include="Microsoft.AspNetCore.DataProtection.Abstractions" Version="10.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="$(IdentityEFCoreVersion)" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="$(RuntimeCompilationVersion)" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="$(MvcRazorRuntimeCompilationVersion)" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="$(FrameworkVersion)" />
<PackageVersion Include="Microsoft.AspNetCore.TestHost" Version="$(FrameworkVersion)" />
<PackageVersion Include="Microsoft.AspNetCore.WebUtilities" Version="2.2.0" />
@ -75,31 +71,27 @@
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="$(EFCoreVersion)" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="$(EFCoreVersion)" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="$(EFCoreVersion)" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="$(FrameworkVersion)" />
<!-- TODO - Upgrade hybrid cache-->
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="$(CachingMemoryVersion)" />
<PackageVersion Include="Microsoft.Extensions.Caching.Hybrid" Version="9.3.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="$(FrameworkVersion)" />
<!-- TODO - Upgrade diagnostics.testing-->
<PackageVersion Include="Microsoft.Extensions.Diagnostics.Testing" Version="8.10.0" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="$(FrameworkVersion)" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="$(FrameworkVersion)" />
<PackageVersion Include="Microsoft.Extensions.Http.Polly" Version="$(FrameworkVersion)" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="9.9.0" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="$(FrameworkVersion)" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="$(LoggingAbstractionsVersion)" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="$(LoggingAbstractionsVersion)" />
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="$(LoggingAbstractionsVersion)" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="$(FrameworkVersion)" />
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="$(FrameworkVersion)" />
<PackageVersion Include="Microsoft.Extensions.Primitives" Version="$(FrameworkVersion)" />
<!-- TODO - Upgrade service discovery and timeprovider.testing-->
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery" Version="9.1.0" />
<PackageVersion Include="Microsoft.Extensions.TimeProvider.Testing" Version="9.0.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Hybrid" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.Testing" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http.Polly" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Primitives" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.TimeProvider.Testing" Version="10.0.0" />
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="$(IdentityModelVersion)" />
<PackageVersion Include="Microsoft.IdentityModel.Logging" Version="$(IdentityModelVersion)" />
<PackageVersion Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="$(IdentityModelVersion)" />
<PackageVersion Include="Microsoft.Net.Http.Headers" Version="9.0.6" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageVersion Include="Microsoft.NETCore.Jit" Version="2.0.8" />
<PackageVersion Include="Microsoft.Playwright.Xunit" Version="1.50.0" />
@ -133,9 +125,11 @@
<PackageVersion Include="Shouldly" Version="4.2.1" />
<PackageVersion Include="SimpleExec" Version="12.0.0" />
<PackageVersion Include="SimpleFeedReader" Version="2.0.4" />
<PackageVersion Include="Spectre.Console.Cli" Version="0.53.0" />
<PackageVersion Include="Spectre.Console.Json" Version="0.53.0" />
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="$(IdentityModelVersion)" />
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
<PackageVersion Include="System.Text.Json" Version="$(SystemTextJsonVersion)" />
<PackageVersion Include="System.Text.Json" Version="10.0.0" />
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
<PackageVersion Include="xunit.core" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />

View file

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<Sdk Name="Aspire.AppHost.Sdk" Version="9.5.0" />
<Sdk Name="Aspire.AppHost.Sdk" Version="9.5.2" />
<PropertyGroup>
<OutputType>Exe</OutputType>

2
global.json vendored
View file

@ -1,6 +1,6 @@
{
"sdk": {
"version": "10.0.100-rc.2.25502.107",
"version": "10.0.100",
"rollForward": "latestMajor",
"allowPrerelease": true
}

View file

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<Sdk Name="Aspire.AppHost.Sdk" Version="9.5.0" />
<Sdk Name="Aspire.AppHost.Sdk" Version="9.5.2" />
<PropertyGroup>
<OutputType>Exe</OutputType>
@ -58,6 +58,9 @@
<ProjectReference Include="..\..\..\hosts\net9\AspNetIdentity9\Host.AspNetIdentity9.csproj" />
<ProjectReference Include="..\..\..\hosts\net9\EntityFramework9\Host.EntityFramework9.csproj" />
<ProjectReference Include="..\..\..\hosts\net9\Main9\Host.Main9.csproj" />
<ProjectReference Include="..\..\..\hosts\net10\AspNetIdentity10\Host.AspNetIdentity10.csproj" />
<ProjectReference Include="..\..\..\hosts\net10\EntityFramework10\Host.EntityFramework10.csproj" />
<ProjectReference Include="..\..\..\hosts\net10\Main10\Host.Main10.csproj" />
<ProjectReference Include="..\..\..\migrations\AspNetIdentityDb\AspNetIdentityDb.csproj" />
<ProjectReference Include="..\..\..\migrations\IdentityServerDb\IdentityServerDb.csproj" />
<ProjectReference Include="..\..\..\templates\src\IdentityServerAspNetIdentity\IdentityServerAspNetIdentity.csproj" />

View file

@ -46,13 +46,25 @@ void ConfigureIdentityServerHosts()
projectRegistry.Add("is-host", hostMain);
}
if (HostIsEnabled(nameof(Projects.Host_Main10)))
{
var hostMain = builder
.AddProject<Projects.Host_Main10>("is-host")
.WithHttpHealthCheck(path: "/.well-known/openid-configuration");
projectRegistry.Add("is-host", hostMain);
}
// These hosts require a database
var dbHosts = new List<string>
{
nameof(Projects.Host_AspNetIdentity8),
nameof(Projects.Host_AspNetIdentity9),
nameof(Projects.Host_AspNetIdentity10),
nameof(Projects.Host_EntityFramework8),
nameof(Projects.Host_EntityFramework9)
nameof(Projects.Host_EntityFramework9),
nameof(Projects.Host_EntityFramework10)
};
if (dbHosts.Any(HostIsEnabled))
@ -100,6 +112,23 @@ void ConfigureIdentityServerHosts()
projectRegistry.Add("is-host", hostAspNetIdentity);
}
if (HostIsEnabled(nameof(Projects.Host_AspNetIdentity10)))
{
var hostAspNetIdentity = builder.AddProject<Projects.Host_AspNetIdentity10>(name: "is-host")
.WithHttpHealthCheck(path: "/.well-known/openid-configuration")
.WithReference(identityServerDb, connectionName: "DefaultConnection");
if (appConfig.RunDatabaseMigrations)
{
var aspnetMigration = builder.AddProject<Projects.AspNetIdentityDb>(name: "aspnetidentitydb-migrations")
.WithReference(identityServerDb, connectionName: "DefaultConnection")
.WaitFor(identityServerDb);
hostAspNetIdentity.WaitForCompletion(aspnetMigration);
}
projectRegistry.Add("is-host", hostAspNetIdentity);
}
if (HostIsEnabled(nameof(Projects.Host_EntityFramework8)))
{
var hostEntityFramework = builder.AddProject<Projects.Host_EntityFramework8>(name: "is-host")
@ -133,6 +162,23 @@ void ConfigureIdentityServerHosts()
projectRegistry.Add("is-host", hostEntityFramework);
}
if (HostIsEnabled(nameof(Projects.Host_EntityFramework10)))
{
var hostEntityFramework = builder.AddProject<Projects.Host_EntityFramework10>(name: "is-host")
.WithHttpHealthCheck(path: "/.well-known/openid-configuration")
.WithReference(identityServerDb, connectionName: "DefaultConnection");
if (appConfig.RunDatabaseMigrations)
{
var idSrvMigration = builder.AddProject<Projects.IdentityServerDb>(name: "identityserverdb-migrations")
.WithReference(identityServerDb, connectionName: "DefaultConnection")
.WaitFor(identityServerDb);
hostEntityFramework.WaitForCompletion(idSrvMigration);
}
projectRegistry.Add("is-host", hostEntityFramework);
}
}
bool HostIsEnabled(string name) =>

View file

@ -7,7 +7,7 @@
}
},
"AspireProjectConfiguration": {
"IdentityHost": "Host_Main9",
"IdentityHost": "Host_Main10",
"UseClients": {
"ConsoleCibaClient": false,
"ConsoleClientCredentialsFlow": false,

View file

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<Sdk Name="Aspire.AppHost.Sdk" Version="9.5.0" />
<Sdk Name="Aspire.AppHost.Sdk" Version="9.5.2" />
<PropertyGroup>
<OutputType>Exe</OutputType>

View file

@ -9,6 +9,8 @@
<PackageReference Include="Duende.IdentityModel.OidcClient" />
<PackageReference Include="Serilog.AspNetCore" />
<PackageReference Include="Spectre.Console.Cli" />
<PackageReference Include="Spectre.Console.Json" />
</ItemGroup>
<ItemGroup>

View file

@ -0,0 +1,10 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
namespace ConsoleResourceIndicators;
internal enum OutputMode
{
Verbose,
Table
}

View file

@ -1,23 +1,60 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using System.Buffers.Text;
using System.Text;
using Clients;
using ConsoleResourceIndicators;
using Duende.IdentityModel.Client;
using Duende.IdentityModel.OidcClient;
using Microsoft.Extensions.Hosting;
using Serilog;
using Spectre.Console;
var builder = Host.CreateApplicationBuilder(args);
// Add ServiceDefaults from Aspire
builder.AddServiceDefaults();
OidcClient _oidcClient;
// Display banner
AnsiConsole.Write(new Rule("[bold green]Resource Indicators Demo[/]").Centered());
AnsiConsole.WriteLine();
"Resource Indicators Demo".ConsoleBox(ConsoleColor.Green);
// Resolve the authority from the configuration
var authority = builder.Configuration["is-host"]
?? throw new InvalidOperationException("Authority configuration 'is-host' is missing.");
// Display important setup information
var setupPanel = new Panel(
new Markup($"[yellow]⚠[/] [bold]Before running tests:[/]\n" +
$"[dim]→[/] Ensure your Identity Server is running at: [cyan]{authority}[/]\n" +
$"[dim]→[/] Sign in to the Identity Server before starting tests\n" +
$"[dim]→[/] This will allow tests to complete quickly and smoothly"))
.Border(BoxBorder.Rounded)
.BorderColor(Color.Yellow)
.Header("[yellow]Setup Checklist[/]");
AnsiConsole.Write(setupPanel);
AnsiConsole.WriteLine();
// Determine output mode based on whether console is interactive
OutputMode mode;
if (Console.IsInputRedirected || Console.IsOutputRedirected || !Environment.UserInteractive)
{
// Non-interactive environment, use verbose mode by default
AnsiConsole.MarkupLine("[dim]Running in non-interactive mode. Using verbose output.[/]");
mode = OutputMode.Verbose;
}
else
{
// Interactive environment, prompt user for output mode
var outputMode = AnsiConsole.Prompt(
new SelectionPrompt<string>()
.Title("[cyan]Choose output mode:[/]")
.AddChoices("Table View (Live Status)", "Verbose Output (Detailed)")
.HighlightStyle(new Style(Color.Green)));
mode = outputMode.StartsWith("Table") ? OutputMode.Table : OutputMode.Verbose;
}
AnsiConsole.WriteLine();
var testRunner = new TestRunner(authority, mode);
var testsToRun = new List<Test>
{
@ -31,138 +68,126 @@ var testsToRun = new List<Test>
new() { Id = "8", Enabled = true, Scope = "resource1.scope1 resource2.scope1 resource3.scope1 shared.scope", Resources = ["urn:resource3"] },
new() { Id = "9", Enabled = true, Scope = "resource3.scope1 offline_access", Resources = ["urn:resource3"] },
new() { Id = "10", Enabled = true, Scope = "resource3.scope1", Resources = ["urn:resource3"] },
new() { Id = "11", Enabled = true, Scope = "resource1.scope1 offline_access", Resources = ["urn:resource3"] },
new() { Id = "12", Enabled = true, Scope = "shared.scope", Resources = ["urn:invalid"] }
new() { Id = "11", Enabled = true, Scope = "resource1.scope1 offline_access", Resources = ["urn:resource3"], AccessTokenExpected = false },
new() { Id = "12", Enabled = true, Scope = "shared.scope", Resources = ["urn:invalid"], AccessTokenExpected = false }
};
foreach (var test in testsToRun.Where(t => t.Enabled))
await testRunner.RunAllTestsAsync(testsToRun);
// Show summary
AnsiConsole.WriteLine();
AnsiConsole.Write(new Rule("[bold green]Test Summary[/]").Centered());
var summary = new Table()
.Border(TableBorder.Rounded)
.AddColumn("[bold]Status[/]")
.AddColumn("[bold]Count[/]");
var completed = testsToRun.Count(t => t.Enabled && t.Status == TestStatus.Completed);
var failed = testsToRun.Count(t => t.Enabled && t.Status == TestStatus.Failed);
var total = testsToRun.Count(t => t.Enabled);
summary.AddRow("[green]Completed[/]", completed.ToString());
summary.AddRow("[red]Failed[/]", failed.ToString());
summary.AddRow("[cyan]Total[/]", total.ToString());
AnsiConsole.Write(summary);
// Show expected errors section
var testsWithExpectedErrors = testsToRun
.Where(t => t.Enabled && t.Result?.RefreshResults.Any(r => r.WasExpectedError) == true)
.ToList();
if (testsWithExpectedErrors.Any())
{
var resources = test.Resources != null ? test.Resources.Aggregate((x, y) => $"{x}, {y}") : "-none-";
($"Runing test: ({test.Id}) SCOPES: " + test.Scope + ", RESOURCES: " + resources).ConsoleBox(ConsoleColor.Green);
AnsiConsole.WriteLine();
AnsiConsole.Write(new Rule("[bold yellow]Expected Errors (By Design)[/]").Centered());
AnsiConsole.WriteLine();
AnsiConsole.MarkupLine("[dim]The following errors were expected as part of the test validation:[/]");
AnsiConsole.WriteLine();
try
var expectedErrorsTable = new Table()
.Border(TableBorder.Rounded)
.AddColumn("[bold]Test ID[/]")
.AddColumn("[bold]Resource[/]")
.AddColumn("[bold]Error[/]")
.AddColumn("[bold]Reason[/]");
foreach (var test in testsWithExpectedErrors)
{
await FrontChannel(test.Scope, test.Resources);
Thread.Sleep(millisecondsTimeout: 1000);
}
catch (Exception ex)
{
Console.WriteLine($"Exception: {ex.Message}");
}
}
var expectedErrors = test.Result!.RefreshResults.Where(r => r.WasExpectedError).ToList();
// Exit the application
"Exiting application...".ConsoleYellow();
Environment.Exit(0);
async Task FrontChannel(string scope, IEnumerable<string> resource)
{
// Resolve the authority from the configuration.
var authority = builder.Configuration["is-host"];
resource ??= [];
// create a redirect URI using an available port on the loopback address.
// requires the OP to allow random ports on 127.0.0.1 - otherwise set a static port
var browser = new SystemBrowser();
var redirectUri = string.Format($"http://127.0.0.1:{browser.Port}");
var options = new OidcClientOptions
{
Authority = authority,
ClientId = "console.resource.indicators",
RedirectUri = redirectUri,
Scope = scope,
Resource = [.. resource],
FilterClaims = false,
LoadProfile = false,
Browser = browser,
Policy =
foreach (var error in expectedErrors)
{
RequireIdentityTokenSignature = false
var reason = "Resource not configured for this test";
expectedErrorsTable.AddRow(
test.Id,
error.Resource.Replace("urn:", ""),
$"[yellow]{error.Error ?? "unknown"}[/]",
$"[dim]{reason}[/]"
);
}
};
var serilog = new LoggerConfiguration()
.MinimumLevel.Warning()
.Enrich.FromLogContext()
.WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level}] {SourceContext}{NewLine}{Message}{NewLine}{Exception}{NewLine}")
.CreateLogger();
options.LoggerFactory.AddSerilog(serilog);
_oidcClient = new OidcClient(options);
var result = await _oidcClient.LoginAsync();
var parts = result.AccessToken.Split('.');
var header = parts[0];
var payload = parts[1];
Console.WriteLine();
Console.WriteLine("Standard access token:");
Console.WriteLine(Encoding.UTF8.GetString(Base64Url.DecodeFromChars(header)).PrettyPrintJson());
Console.WriteLine(Encoding.UTF8.GetString(Base64Url.DecodeFromChars(payload)).PrettyPrintJson());
if (result.RefreshToken == null)
{
Console.WriteLine();
Console.WriteLine("No Refresh Token, exiting.");
Environment.Exit(0);
}
await BackChannel(result);
AnsiConsole.Write(expectedErrorsTable);
}
async Task BackChannel(LoginResult result)
// Show detailed results if available
if (mode == OutputMode.Table)
{
Console.WriteLine("\n\n");
Console.WriteLine("Refreshing with resource parameters");
var testsWithResults = testsToRun.Where(t => t.Enabled && t.Result != null).ToList();
var resources = new List<string>() { "urn:resource1", "urn:resource2", "urn:resource3" };
foreach (var resource in resources)
if (testsWithResults.Any())
{
$"Refreshing for resource: {resource}...".ConsoleGreen();
await Refresh(result.RefreshToken, resource);
AnsiConsole.WriteLine();
AnsiConsole.Write(new Rule("[bold cyan]Detailed Test Results[/]").Centered());
Thread.Sleep(millisecondsTimeout: 500);
}
}
var detailsTable = new Table()
.Border(TableBorder.Rounded)
.AddColumn("[bold]Test ID[/]")
.AddColumn("[bold]Access Token[/]")
.AddColumn("[bold]Refresh Token[/]")
.AddColumn("[bold]Refresh Operations[/]");
async Task Refresh(string refreshToken, string resource)
{
var result = await _oidcClient.RefreshTokenAsync(refreshToken,
new Parameters
foreach (var test in testsWithResults)
{
{ "resource", resource }
});
var accessToken = test.Result!.AccessTokenReceived
? "[green]✓ Received[/]"
: test.AccessTokenExpected
? "[red]✗ Not Received[/]"
: "[green]✓ Not Expected[/]";
if (result.IsError)
{
Console.WriteLine();
Console.WriteLine(result.Error);
return;
var refreshToken = test.Result.RefreshTokenReceived
? "[green]✓ Received[/]"
: test.RefreshTokenExpected
? "[red]✗ Not Received[/]"
: "[green]✓ Not Expected[/]";
var refreshOps = test.Result.RefreshResults.Any()
? $"{test.Result.RefreshResults.Count(r => r.Success)}/{test.Result.RefreshResults.Count} successful"
: "-";
detailsTable.AddRow(
test.Id,
accessToken,
refreshToken,
refreshOps
);
}
AnsiConsole.Write(detailsTable);
}
Console.WriteLine();
Console.WriteLine("down-scoped access token:");
var parts = result.AccessToken.Split('.');
var header = parts[0];
var payload = parts[1];
Console.WriteLine(Encoding.UTF8.GetString(Base64Url.DecodeFromChars(header)).PrettyPrintJson());
Console.WriteLine(Encoding.UTF8.GetString(Base64Url.DecodeFromChars(payload)).PrettyPrintJson());
}
internal class Test
// Exit prompt - only in interactive mode
if (Environment.UserInteractive && !Console.IsInputRedirected)
{
public string Id { get; set; }
public bool Enabled { get; set; }
public string Scope { get; set; }
public IEnumerable<string> Resources { get; set; } = null;
AnsiConsole.WriteLine();
AnsiConsole.Markup("[dim]Press Enter to exit...[/]");
Console.ReadLine();
}
else
{
Environment.Exit(0);
}

View file

@ -0,0 +1,42 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
namespace ConsoleResourceIndicators;
internal enum TestStatus
{
Pending,
Running,
Completed,
Failed
}
internal class TestResult
{
public bool AccessTokenReceived { get; set; }
public bool RefreshTokenReceived { get; set; }
public List<RefreshResult> RefreshResults { get; set; } = [];
}
internal class RefreshResult
{
public string Resource { get; set; } = string.Empty;
public bool Success { get; set; }
public string Error { get; set; }
public bool WasExpectedError { get; set; }
}
internal class Test
{
public string Id { get; set; } = string.Empty;
public bool Enabled { get; set; }
public bool AccessTokenExpected { get; set; } = true;
public bool RefreshTokenExpected => Scope.Contains("offline_access") && AccessTokenExpected;
public string Scope { get; set; } = string.Empty;
public IEnumerable<string> Resources { get; set; } = [];
public TestStatus Status { get; set; } = TestStatus.Pending;
public string ErrorMessage { get; set; }
public DateTime? StartTime { get; set; }
public DateTime? EndTime { get; set; }
public TestResult Result { get; set; }
}

View file

@ -0,0 +1,360 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using System.Buffers.Text;
using System.Text;
using Duende.IdentityModel.Client;
using Duende.IdentityModel.OidcClient;
using Spectre.Console;
using Spectre.Console.Json;
namespace ConsoleResourceIndicators;
internal class TestRunner(string authority, OutputMode outputMode)
{
private readonly string _authority = authority;
private readonly OutputMode _outputMode = outputMode;
private OidcClient _oidcClient;
private static readonly string[] RefreshResources = ["urn:resource1", "urn:resource2", "urn:resource3"];
private const int DelayBetweenTestsMs = 1000;
private const int DelayBetweenRefreshMs = 500;
public async Task RunAllTestsAsync(List<Test> tests)
{
if (_outputMode == OutputMode.Table)
{
await RunTestsWithTableAsync(tests);
}
else
{
await RunTestsVerboseAsync(tests);
}
}
private async Task RunTestsVerboseAsync(List<Test> tests)
{
foreach (var test in tests.Where(t => t.Enabled))
{
await RunTestVerboseAsync(test);
}
}
private async Task RunTestsWithTableAsync(List<Test> tests)
{
var enabledTests = tests.Where(t => t.Enabled).ToList();
await AnsiConsole.Live(CreateTestTable(enabledTests))
.StartAsync(async ctx =>
{
foreach (var test in enabledTests)
{
test.Status = TestStatus.Running;
test.StartTime = DateTime.Now;
ctx.UpdateTarget(CreateTestTable(enabledTests));
try
{
await ExecuteTestAsync(test, verbose: false);
test.Status = TestStatus.Completed;
}
catch (Exception ex)
{
test.Status = TestStatus.Failed;
test.ErrorMessage = ex.Message;
}
test.EndTime = DateTime.Now;
ctx.UpdateTarget(CreateTestTable(enabledTests));
await Task.Delay(DelayBetweenTestsMs);
}
});
}
private static Table CreateTestTable(List<Test> tests)
{
var table = new Table()
.Border(TableBorder.Rounded)
.AddColumn("[bold]ID[/]")
.AddColumn("[bold]Status[/]")
.AddColumn("[bold]Scopes[/]")
.AddColumn("[bold]Resources[/]")
.AddColumn("[bold]Duration[/]");
foreach (var test in tests)
{
var status = test.Status switch
{
TestStatus.Pending => "[grey]Pending[/]",
TestStatus.Running => "[yellow]Running...[/]",
TestStatus.Completed => "[green]✓ Completed[/]",
TestStatus.Failed => $"[red]✗ Failed[/]",
_ => "[grey]Unknown[/]"
};
var resourcesList = test.Resources?.Any() == true
? string.Join(", ", test.Resources.Select(r => r.Replace("urn:", "")))
: "-";
var duration = test.StartTime.HasValue && test.EndTime.HasValue
? $"{(test.EndTime.Value - test.StartTime.Value).TotalSeconds:F1}s"
: test.StartTime.HasValue
? "..."
: "-";
// Truncate scopes for table display
var scopeDisplay = test.Scope.Length > 40
? string.Concat(test.Scope.AsSpan(0, 37), "...")
: test.Scope;
table.AddRow(
test.Id,
status,
scopeDisplay,
resourcesList,
duration
);
}
return table;
}
private async Task RunTestVerboseAsync(Test test)
{
var resourcesList = test.Resources?.Any() == true
? string.Join(", ", test.Resources)
: "-none-";
// Escape the text to prevent Spectre.Console from interpreting it as markup
var scopeText = test.Scope.EscapeMarkup();
var resourcesText = resourcesList.EscapeMarkup();
var panel = new Panel(
new Markup($"[bold]Test {test.Id}[/]\n" +
$"[dim]Scopes:[/] {scopeText}\n" +
$"[dim]Resources:[/] {resourcesText}"))
.Border(BoxBorder.Rounded)
.BorderColor(Color.Blue)
.Header("[blue]Running Test[/]");
AnsiConsole.Write(panel);
try
{
test.Status = TestStatus.Running;
test.StartTime = DateTime.Now;
await ExecuteTestAsync(test, verbose: true);
test.Status = TestStatus.Completed;
test.EndTime = DateTime.Now;
AnsiConsole.MarkupLine("[green]✓ Test completed successfully[/]\n");
await Task.Delay(DelayBetweenTestsMs);
}
catch (Exception ex)
{
test.Status = TestStatus.Failed;
test.ErrorMessage = ex.Message;
test.EndTime = DateTime.Now;
AnsiConsole.MarkupLine($"[red]✗ Test failed: {Markup.Escape(ex.Message)}[/]\n");
}
}
private async Task ExecuteTestAsync(Test test, bool verbose)
{
test.Result = new TestResult();
var browser = new SystemBrowser();
var redirectUri = $"http://127.0.0.1:{browser.Port}";
var options = new OidcClientOptions
{
Authority = _authority,
ClientId = "console.resource.indicators",
RedirectUri = redirectUri,
Scope = test.Scope,
Resource = test.Resources?.ToList() ?? [],
FilterClaims = false,
LoadProfile = false,
Browser = browser,
Policy =
{
RequireIdentityTokenSignature = false
}
};
_oidcClient = new OidcClient(options);
var result = await _oidcClient.LoginAsync();
test.Result.AccessTokenReceived = result.AccessToken != null;
if (verbose)
{
HandleAccessTokenVerbose(result.AccessToken, test.AccessTokenExpected);
if (test.AccessTokenExpected && test.RefreshTokenExpected)
{
test.Result.RefreshTokenReceived = result.RefreshToken != null;
await HandleRefreshTokenVerbose(result, test.Resources, test.Result);
}
else if (!test.RefreshTokenExpected)
{
AnsiConsole.MarkupLine("[green]✓ Refresh Token was not expected and not received[/]");
}
}
else
{
// In table mode, just validate and collect results without output
if (test.AccessTokenExpected && result.AccessToken == null)
{
throw new Exception("Access token expected but not received");
}
if (test.AccessTokenExpected && test.RefreshTokenExpected)
{
if (result.RefreshToken == null)
{
throw new Exception("Refresh token expected but not received");
}
test.Result.RefreshTokenReceived = true;
await HandleRefreshTokenSilent(result, test.Resources, test.Result);
}
}
}
private static void HandleAccessTokenVerbose(string accessToken, bool expected)
{
if (expected)
{
if (accessToken is null)
{
AnsiConsole.MarkupLine("[red]✗ An Access Token was expected but not received[/]");
return;
}
AnsiConsole.MarkupLine("[green]✓ Access Token received[/]");
AnsiConsole.WriteLine();
PrintJwtToken(accessToken, "Standard Access Token");
}
else
{
AnsiConsole.MarkupLine("[green]✓ Access Token was not expected and not received[/]");
}
}
private async Task HandleRefreshTokenVerbose(LoginResult result, IEnumerable<string> testResources, TestResult testResult)
{
if (result.RefreshToken is null)
{
AnsiConsole.MarkupLine("[red]✗ A Refresh Token was expected but not received[/]");
return;
}
AnsiConsole.WriteLine();
AnsiConsole.WriteLine();
AnsiConsole.Write(new Rule("[yellow]Refreshing with Resource Parameters[/]").LeftJustified());
var resourcesSet = testResources?.ToHashSet() ?? [];
foreach (var resource in RefreshResources)
{
AnsiConsole.MarkupLine($"[cyan]→ Refreshing for resource: {resource}[/]");
await RefreshTokenAsync(result.RefreshToken, resource, resourcesSet.Contains(resource), verbose: true, testResult);
await Task.Delay(DelayBetweenRefreshMs);
}
}
private async Task HandleRefreshTokenSilent(LoginResult result, IEnumerable<string> testResources, TestResult testResult)
{
var resourcesSet = testResources?.ToHashSet() ?? [];
foreach (var resource in RefreshResources)
{
await RefreshTokenAsync(result.RefreshToken!, resource, resourcesSet.Contains(resource), verbose: false, testResult);
await Task.Delay(DelayBetweenRefreshMs);
}
}
private async Task RefreshTokenAsync(string refreshToken, string resource, bool resourceIsConfigured, bool verbose, TestResult testResult)
{
if (_oidcClient == null)
{
throw new InvalidOperationException("OIDC client not initialized");
}
var result = await _oidcClient.RefreshTokenAsync(refreshToken,
new Parameters
{
{ "resource", resource }
});
var refreshResult = new RefreshResult
{
Resource = resource,
Success = !result.IsError,
Error = result.Error,
WasExpectedError = !resourceIsConfigured && result.IsError
};
testResult.RefreshResults.Add(refreshResult);
if (result.IsError)
{
if (resourceIsConfigured)
{
var message = $"An error was not expected but was received: {result.Error}";
if (verbose)
{
AnsiConsole.MarkupLine($"[red]✗ {Markup.Escape(message)}[/]");
}
else
{
throw new Exception(message);
}
}
else if (verbose)
{
// Expected error - show in verbose mode
AnsiConsole.MarkupLine($"[green]✓ Expected error received: [/][yellow]{Markup.Escape(result.Error ?? "unknown")}[/]");
}
// In non-verbose mode, we don't show expected errors here - they'll be in the summary
return;
}
if (verbose)
{
AnsiConsole.WriteLine();
PrintJwtToken(result.AccessToken!, "Down-scoped access token");
}
}
private static void PrintJwtToken(string token, string blockHeader = "JWT Token")
{
var parts = token.Split('.');
if (parts.Length < 2)
{
AnsiConsole.MarkupLine("[red]Invalid JWT token format[/]");
return;
}
var header = parts[0];
var payload = parts[1];
var headerJson = Encoding.UTF8.GetString(Base64Url.DecodeFromChars(header));
var payloadJson = Encoding.UTF8.GetString(Base64Url.DecodeFromChars(payload));
// Use Spectre.Console's built-in JSON rendering with proper namespace
AnsiConsole.Write(
new Panel(
new Rows(
new Markup("[bold yellow]Header:[/]"),
new JsonText(headerJson),
new Text(""),
new Markup("[bold yellow]Payload:[/]"),
new JsonText(payloadJson)
))
.Border(BoxBorder.Rounded)
.BorderColor(Color.Grey)
.Header($"[dim]{blockHeader}[/]"));
}
}

View file

@ -34,12 +34,12 @@ response.Show();
var refresh_token = response.RefreshToken;
while (true)
for (var i = 0; i < 10; i++)
{
response = await RefreshTokenAsync(refresh_token);
response.Show();
Thread.Sleep(5000);
Thread.Sleep(50);
await CallServiceAsync(response.AccessToken);

View file

@ -36,8 +36,8 @@ var plannedRuns = new List<PlannedRun>
new() { Enabled = true, Id = "J", Name = "No scope (resource: resource1)", Scope = "", Resource = "urn:resource1" },
new() { Enabled = true, Id = "K", Name = "No scope (resource: resource3)", Scope = "", Resource = "urn:resource3" },
new() { Enabled = true, Id = "L", Name = "Isolated scope without resource parameter", Scope = "resource3.scope1" },
new() { Enabled = true, Id = "M", Name = "Isolated scope without resource parameter", Scope = "resource3.scope1", Resource = "urn:resource3" },
new() { Enabled = true, Id = "N", Name = "Isolated scope without resource parameter", Scope = "resource3.scope1", Resource = "urn:resource2" }
new() { Enabled = true, Id = "M", Name = "Isolated scope with resource parameter", Scope = "resource3.scope1", Resource = "urn:resource3" },
new() { Enabled = true, Id = "N", Name = "Shared scope with resource parameter", Scope = "shared.scope", Resource = "urn:resource2" }
};
// Execute the planned runs

View file

@ -74,7 +74,7 @@ internal class Program
{
Authority = Constants.Authority,
ClientId = "winconsole",
Scope = "openid profile scope1",
Scope = "openid profile resource1.scope1",
RedirectUri = redirectUri,
};
@ -94,7 +94,12 @@ internal class Program
var callbackManager = new CallbackManager(state.State);
// open system browser to start authentication
Process.Start(state.StartUrl);
var psi = new ProcessStartInfo
{
FileName = state.StartUrl,
UseShellExecute = true
};
Process.Start(psi);
Console.WriteLine("Running callback manager");
var response = await callbackManager.RunServer();

View file

@ -1,7 +1,6 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using System.Reflection;
using System.Runtime.Versioning;
using Microsoft.Win32;
@ -36,7 +35,7 @@ internal class RegistryConfig
private const string CommandKeyValueName = "";
private const string CommandKeyValueFormat = "\"{0}\" \"%1\"";
private static string CommandKeyValueValue => string.Format(CommandKeyValueFormat, Assembly.GetExecutingAssembly().Location);
private static string CommandKeyValueValue => string.Format(CommandKeyValueFormat, Environment.ProcessPath);
private const string UrlProtocolValueName = "URL Protocol";
private const string UrlProtocolValueValue = "";

View file

@ -269,7 +269,7 @@ public static class ClientsConsole
RedirectUris = { "sample-windows-client://callback" },
RequireConsent = false,
AllowOfflineAccess = true,
AllowedIdentityTokenSigningAlgorithms = { "ES256" },
AllowedIdentityTokenSigningAlgorithms = { "RS256" },
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,

View file

@ -7,7 +7,7 @@ namespace Duende.IdentityServer.Hosts.Shared.Configuration;
public static class TestClients
{
public static IEnumerable<Client> Get()
public static List<Client> Get()
{
var clients = new List<Client>();

View file

@ -25,7 +25,7 @@ public sealed class CustomClientRegistrationProcessor(
var clientId = clientIdParameter.ToString();
if (clientId != null)
{
var existingClient = clientStore.FindClientByIdAsync(clientId);
var existingClient = await clientStore.FindClientByIdAsync(clientId);
if (existingClient is not null)
{
return new DynamicClientRegistrationError(

View file

@ -2,6 +2,7 @@
// See LICENSE in the project root for license information.
using Duende.IdentityServer;
using Duende.IdentityServer.Configuration;
using Duende.IdentityServer.UI;
using Duende.IdentityServer.UI.AspNetIdentity.Models;
using IdentityServerHost.Data;
@ -122,6 +123,9 @@ internal static class HostingExtensions
app.MapRazorPages()
.RequireAuthorization();
app.MapDynamicClientRegistration()
.AllowAnonymous();
return app;
}
}

View file

@ -1,7 +1,10 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using Duende.IdentityServer.Configuration;
using Duende.IdentityServer.Configuration.RequestProcessing;
using Duende.IdentityServer.Hosts.Shared.Configuration;
using Duende.IdentityServer.Hosts.Shared.Customization;
using Duende.IdentityServer.UI.AspNetIdentity.Models;
namespace IdentityServerHost;
@ -26,6 +29,10 @@ internal static class IdentityServerExtensions
.AddInMemoryClients(TestClients.Get())
.AddAspNetIdentity<ApplicationUser>();
builder.Services.AddIdentityServerConfiguration(opt => { })
.AddInMemoryClientConfigurationStore();
builder.Services.AddTransient<IDynamicClientRegistrationRequestProcessor, CustomClientRegistrationProcessor>();
return builder;
}
}

View file

@ -4,6 +4,7 @@
using System.Globalization;
using System.Security.Claims;
using Duende.IdentityServer;
using Duende.IdentityServer.Configuration;
using Duende.IdentityServer.Hosts.Shared.Customization;
using Duende.IdentityServer.UI;
using Microsoft.AspNetCore.Mvc.Razor;
@ -171,6 +172,9 @@ internal static class HostingExtensions
app.MapRazorPages()
.RequireAuthorization();
app.MapDynamicClientRegistration()
.AllowAnonymous();
return app;
}
}

View file

@ -5,6 +5,7 @@ using System.Security.Cryptography.X509Certificates;
using Duende.IdentityModel;
using Duende.IdentityServer;
using Duende.IdentityServer.Configuration;
using Duende.IdentityServer.Configuration.RequestProcessing;
using Duende.IdentityServer.Hosts.Shared.Configuration;
using Duende.IdentityServer.Hosts.Shared.Customization;
using Microsoft.AspNetCore.Authentication.Certificate;
@ -51,7 +52,7 @@ internal static class IdentityServerExtensions
options.Diagnostics.ChunkSize = 1024 * 1000 - 32; // 1 MB minus some formatting space;
})
.AddServerSideSessions()
.AddInMemoryClients([.. TestClients.Get()])
.AddInMemoryClients(TestClients.Get())
.AddInMemoryIdentityResources(TestResources.IdentityResources)
.AddInMemoryApiResources(TestResources.ApiResources)
.AddInMemoryApiScopes(TestResources.ApiScopes)
@ -81,6 +82,7 @@ internal static class IdentityServerExtensions
builder.Services.AddIdentityServerConfiguration(opt => { })
.AddInMemoryClientConfigurationStore();
builder.Services.AddTransient<IDynamicClientRegistrationRequestProcessor, CustomClientRegistrationProcessor>();
builder.Services.AddDistributedMemoryCache();

View file

@ -2,6 +2,7 @@
// See LICENSE in the project root for license information.
using Duende.IdentityServer;
using Duende.IdentityServer.Configuration;
using Duende.IdentityServer.UI;
using Duende.IdentityServer.UI.AspNetIdentity.Models;
using IdentityServerHost.Data;
@ -120,6 +121,9 @@ internal static class HostingExtensions
app.MapRazorPages()
.RequireAuthorization();
app.MapDynamicClientRegistration()
.AllowAnonymous();
return app;
}
}

View file

@ -1,7 +1,10 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using Duende.IdentityServer.Configuration;
using Duende.IdentityServer.Configuration.RequestProcessing;
using Duende.IdentityServer.Hosts.Shared.Configuration;
using Duende.IdentityServer.Hosts.Shared.Customization;
using Duende.IdentityServer.UI.AspNetIdentity.Models;
namespace IdentityServerHost;
@ -26,6 +29,10 @@ internal static class IdentityServerExtensions
.AddInMemoryClients(TestClients.Get())
.AddAspNetIdentity<ApplicationUser>();
builder.Services.AddIdentityServerConfiguration(opt => { })
.AddInMemoryClientConfigurationStore();
builder.Services.AddTransient<IDynamicClientRegistrationRequestProcessor, CustomClientRegistrationProcessor>();
return builder;
}
}

View file

@ -4,6 +4,7 @@
using System.Globalization;
using System.Security.Claims;
using Duende.IdentityServer;
using Duende.IdentityServer.Configuration;
using Duende.IdentityServer.Hosts.Shared.Customization;
using Duende.IdentityServer.UI;
using Microsoft.AspNetCore.Mvc.Razor;
@ -171,6 +172,9 @@ internal static class HostingExtensions
app.MapRazorPages()
.RequireAuthorization();
app.MapDynamicClientRegistration()
.AllowAnonymous();
return app;
}
}

View file

@ -5,6 +5,7 @@ using System.Security.Cryptography.X509Certificates;
using Duende.IdentityModel;
using Duende.IdentityServer;
using Duende.IdentityServer.Configuration;
using Duende.IdentityServer.Configuration.RequestProcessing;
using Duende.IdentityServer.Hosts.Shared.Configuration;
using Duende.IdentityServer.Hosts.Shared.Customization;
using Microsoft.AspNetCore.Authentication.Certificate;
@ -51,7 +52,7 @@ internal static class IdentityServerExtensions
options.Diagnostics.ChunkSize = 1024 * 1000 - 32; // 1 MB minus some formatting space;
})
.AddServerSideSessions()
.AddInMemoryClients([.. TestClients.Get()])
.AddInMemoryClients(TestClients.Get())
.AddInMemoryIdentityResources(TestResources.IdentityResources)
.AddInMemoryApiResources(TestResources.ApiResources)
.AddInMemoryApiScopes(TestResources.ApiScopes)
@ -81,6 +82,7 @@ internal static class IdentityServerExtensions
builder.Services.AddIdentityServerConfiguration(opt => { })
.AddInMemoryClientConfigurationStore();
builder.Services.AddTransient<IDynamicClientRegistrationRequestProcessor, CustomClientRegistrationProcessor>();
builder.Services.AddDistributedMemoryCache();

View file

@ -2,6 +2,7 @@
// See LICENSE in the project root for license information.
using Duende.IdentityServer;
using Duende.IdentityServer.Configuration;
using Duende.IdentityServer.UI;
using Duende.IdentityServer.UI.AspNetIdentity.Models;
using IdentityServerHost.Data;
@ -122,6 +123,9 @@ internal static class HostingExtensions
app.MapRazorPages()
.RequireAuthorization();
app.MapDynamicClientRegistration()
.AllowAnonymous();
return app;
}
}

View file

@ -1,7 +1,10 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using Duende.IdentityServer.Configuration;
using Duende.IdentityServer.Configuration.RequestProcessing;
using Duende.IdentityServer.Hosts.Shared.Configuration;
using Duende.IdentityServer.Hosts.Shared.Customization;
using Duende.IdentityServer.UI.AspNetIdentity.Models;
namespace IdentityServerHost;
@ -26,6 +29,10 @@ internal static class IdentityServerExtensions
.AddInMemoryClients(TestClients.Get())
.AddAspNetIdentity<ApplicationUser>();
builder.Services.AddIdentityServerConfiguration(opt => { })
.AddInMemoryClientConfigurationStore();
builder.Services.AddTransient<IDynamicClientRegistrationRequestProcessor, CustomClientRegistrationProcessor>();
return builder;
}
}

View file

@ -4,6 +4,7 @@
using System.Globalization;
using System.Security.Claims;
using Duende.IdentityServer;
using Duende.IdentityServer.Configuration;
using Duende.IdentityServer.Hosts.Shared.Customization;
using Duende.IdentityServer.UI;
using Microsoft.AspNetCore.Mvc.Razor;
@ -171,6 +172,9 @@ internal static class HostingExtensions
app.MapRazorPages()
.RequireAuthorization();
app.MapDynamicClientRegistration()
.AllowAnonymous();
return app;
}
}

View file

@ -5,6 +5,7 @@ using System.Security.Cryptography.X509Certificates;
using Duende.IdentityModel;
using Duende.IdentityServer;
using Duende.IdentityServer.Configuration;
using Duende.IdentityServer.Configuration.RequestProcessing;
using Duende.IdentityServer.Hosts.Shared.Configuration;
using Duende.IdentityServer.Hosts.Shared.Customization;
using Microsoft.AspNetCore.Authentication.Certificate;
@ -51,7 +52,7 @@ internal static class IdentityServerExtensions
options.Diagnostics.ChunkSize = 1024 * 1000 - 32; // 1 MB minus some formatting space;
})
.AddServerSideSessions()
.AddInMemoryClients([.. TestClients.Get()])
.AddInMemoryClients(TestClients.Get())
.AddInMemoryIdentityResources(TestResources.IdentityResources)
.AddInMemoryApiResources(TestResources.ApiResources)
.AddInMemoryApiScopes(TestResources.ApiScopes)
@ -81,6 +82,7 @@ internal static class IdentityServerExtensions
builder.Services.AddIdentityServerConfiguration(opt => { })
.AddInMemoryClientConfigurationStore();
builder.Services.AddTransient<IDynamicClientRegistrationRequestProcessor, CustomClientRegistrationProcessor>();
builder.Services.AddDistributedMemoryCache();

View file

@ -95,7 +95,7 @@ public class DynamicClientRegistrationRequestProcessor : IDynamicClientRegistrat
protected virtual async Task<IStepResult> AddClientSecret(
DynamicClientRegistrationContext context)
{
if (context.Client.ClientSecrets.Count == 0)
if (context.Client.ClientSecrets.Count == 0 && context.Request.TokenEndpointAuthenticationMethod != "none")
{
var (secret, plainText) = await GenerateSecret(context);
context.Items["secret"] = secret;

View file

@ -129,6 +129,12 @@ public class DynamicClientRegistrationValidator : IDynamicClientRegistrationVali
if (context.Request.GrantTypes.Contains(OidcConstants.GrantTypes.ClientCredentials))
{
if (context.Request.RequireClientSecret is false ||
context.Request.TokenEndpointAuthenticationMethod == "none")
{
return StepResult.Failure("client secret is required for client credentials grant type");
}
context.Client.AllowedGrantTypes.Add(GrantType.ClientCredentials);
}
if (context.Request.GrantTypes.Contains(OidcConstants.GrantTypes.AuthorizationCode))
@ -482,6 +488,11 @@ public class DynamicClientRegistrationValidator : IDynamicClientRegistrationVali
{
context.Client.RequireClientSecret = context.Request.RequireClientSecret.Value;
}
else if (context.Request.TokenEndpointAuthenticationMethod == "none")
{
context.Client.RequireClientSecret = false;
}
return StepResult.Success();
}

View file

@ -236,11 +236,10 @@ public static class IdentityServerBuilderExtensionsCore
builder.Services.AddSingleton<IDiagnosticEntry, ClientInfoDiagnosticEntry>();
builder.Services.AddSingleton<ResourceLoadedTracker>();
builder.Services.AddSingleton<IDiagnosticEntry, ResourceInfoDiagnosticEntry>();
builder.Services.AddSingleton(serviceProvider => new DiagnosticSummary(
builder.Services.AddSingleton<DiagnosticSummary>();
builder.Services.AddSingleton(serviceProvider => new DiagnosticDataService(
DateTime.UtcNow,
serviceProvider.GetServices<IDiagnosticEntry>(),
serviceProvider.GetRequiredService<IdentityServerOptions>(),
serviceProvider.GetRequiredService<ILoggerFactory>()));
serviceProvider.GetServices<IDiagnosticEntry>()));
builder.Services.AddHostedService<DiagnosticHostedService>();
return builder;

View file

@ -110,7 +110,7 @@ internal static class Constants
OidcConstants.PromptModes.Consent,
OidcConstants.PromptModes.SelectAccount,
// Create not in here by default -- it's added if customer sets the CreateAccountUrl user interaction option
//OidcConstants.PromptModes.Create,
//OidcConstants.PromptModes.Create,
};
/// <summary>
@ -222,6 +222,7 @@ internal static class Constants
public const string IdentityServerBasePath = "idsvr:IdentityServerBasePath";
public const string SignOutCalled = "idsvr:IdentityServerSignOutCalled";
public const string DetectedExpiredUserSession = "idsvr:IdentityServerDetectedExpiredUserSession";
public const string BackChannlLogoutTriggered = "idsvr:IdentityServerBackChannlLogoutTriggered";
}
public static class TokenTypeHints

View file

@ -223,6 +223,19 @@ public class AuthorizeHttpWriter : IHttpResponseWriter<AuthorizeResult>
await uiLocalesService.StoreUiLocalesForRedirectAsync(response.Request?.UiLocales);
}
var errorModel = await CreateErrorMessage(response, context);
var message = new Message<ErrorMessage>(errorModel, _clock.UtcNow.UtcDateTime);
var id = await _errorMessageStore.WriteAsync(message);
var errorUrl = _options.UserInteraction.ErrorUrl;
var url = errorUrl.AddQueryString(_options.UserInteraction.ErrorIdParameter, id);
context.Response.Redirect(_urls.GetAbsoluteUrl(url));
}
protected virtual Task<ErrorMessage> CreateErrorMessage(AuthorizeResponse response, HttpContext context)
{
var errorModel = new ErrorMessage
{
ActivityId = System.Diagnostics.Activity.Current?.Id,
@ -234,12 +247,6 @@ public class AuthorizeHttpWriter : IHttpResponseWriter<AuthorizeResult>
ClientId = response.Request?.ClientId
};
var message = new Message<ErrorMessage>(errorModel, _clock.UtcNow.UtcDateTime);
var id = await _errorMessageStore.WriteAsync(message);
var errorUrl = _options.UserInteraction.ErrorUrl;
var url = errorUrl.AddQueryString(_options.UserInteraction.ErrorIdParameter, id);
context.Response.Redirect(_urls.GetAbsoluteUrl(url));
return Task.FromResult(errorModel);
}
}

View file

@ -23,6 +23,14 @@ public static class HttpContextExtensions
internal static bool GetSignOutCalled(this HttpContext context) => context.Items.ContainsKey(Constants.EnvironmentKeys.SignOutCalled);
internal static void SetBackChannelLogoutTriggered(this HttpContext context)
{
ArgumentNullException.ThrowIfNull(context);
context.Items[Constants.EnvironmentKeys.BackChannlLogoutTriggered] = "true";
}
internal static bool GetBackChannelLogoutTriggered(this HttpContext context) => context.Items.ContainsKey(Constants.EnvironmentKeys.BackChannlLogoutTriggered);
internal static void SetExpiredUserSession(this HttpContext context, UserSession userSession)
{
ArgumentNullException.ThrowIfNull(context);

View file

@ -7,6 +7,7 @@ using System.Security.Claims;
using Duende.IdentityModel;
using Duende.IdentityServer.Configuration.DependencyInjection;
using Duende.IdentityServer.Extensions;
using Duende.IdentityServer.Models;
using Duende.IdentityServer.Services;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
@ -27,6 +28,8 @@ internal class IdentityServerAuthenticationService : IAuthenticationService
private readonly IAuthenticationSchemeProvider _schemes;
private readonly IClock _clock;
private readonly IUserSession _session;
private readonly IIssuerNameService _issuerNameService;
private readonly ISessionCoordinationService _sessionCoordinationService;
private readonly ILogger<IdentityServerAuthenticationService> _logger;
public IdentityServerAuthenticationService(
@ -34,6 +37,8 @@ internal class IdentityServerAuthenticationService : IAuthenticationService
IAuthenticationSchemeProvider schemes,
IClock clock,
IUserSession session,
IIssuerNameService issuerNameService,
ISessionCoordinationService sessionCoordinationService,
ILogger<IdentityServerAuthenticationService> logger)
{
_inner = decorator.Instance;
@ -41,6 +46,8 @@ internal class IdentityServerAuthenticationService : IAuthenticationService
_schemes = schemes;
_clock = clock;
_session = session;
_issuerNameService = issuerNameService;
_sessionCoordinationService = sessionCoordinationService;
_logger = logger;
}
@ -77,6 +84,38 @@ internal class IdentityServerAuthenticationService : IAuthenticationService
{
// this sets a flag used by middleware to do post-signout work.
context.SetSignOutCalled();
if (!context.GetBackChannelLogoutTriggered())
{
// Note: it is important the work for triggering back-channel logout
// is inside the Response.OnStarting event. Otherwise, in some conditions
// the request will never complete.
// See: https://github.com/DuendeArchive/IdentityServer4/issues/4644
context.Response.OnStarting(async () =>
{
_logger.LogDebug("SignOutCalled set; processing post-signout session cleanup.");
// back channel logout
var user = await _session.GetUserAsync();
if (user != null)
{
var session = new UserSession
{
SubjectId = user.GetSubjectId(),
SessionId = await _session.GetSessionIdAsync(),
DisplayName = user.GetDisplayName(),
ClientIds = (await _session.GetClientListAsync()).ToList(),
Issuer = await _issuerNameService.GetCurrentAsync()
};
await _sessionCoordinationService.ProcessLogoutAsync(session);
}
// this clears our session id cookie so JS clients can detect the user has signed out
await _session.RemoveSessionIdCookieAsync();
});
context.SetBackChannelLogoutTriggered();
}
}
await _inner.SignOutAsync(context, scheme, properties);

View file

@ -7,7 +7,6 @@ using Duende.IdentityServer.Events;
using Duende.IdentityServer.Extensions;
using Duende.IdentityServer.Licensing.V2;
using Duende.IdentityServer.Logging;
using Duende.IdentityServer.Models;
using Duende.IdentityServer.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
@ -61,28 +60,6 @@ public class IdentityServerMiddleware
context.Response.OnStarting(async () =>
{
if (context.GetSignOutCalled())
{
_sanitizedLogger.LogDebug("SignOutCalled set; processing post-signout session cleanup.");
// this clears our session id cookie so JS clients can detect the user has signed out
await userSession.RemoveSessionIdCookieAsync();
var user = await userSession.GetUserAsync();
if (user != null)
{
var session = new UserSession
{
SubjectId = user.GetSubjectId(),
SessionId = await userSession.GetSessionIdAsync(),
DisplayName = user.GetDisplayName(),
ClientIds = (await userSession.GetClientListAsync()).ToList(),
Issuer = await issuerNameService.GetCurrentAsync()
};
await sessionCoordinationService.ProcessLogoutAsync(session);
}
}
if (context.TryGetExpiredUserSession(out var expiredUserSession))
{
_sanitizedLogger.LogDebug("Detected expired session removed; processing post-expiration cleanup.");

View file

@ -1,36 +1,21 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using System.Buffers;
using System.Text;
using System.Text.Json;
using Duende.IdentityServer.Configuration;
using Duende.IdentityServer.Services;
using Microsoft.Extensions.Logging;
namespace Duende.IdentityServer.Licensing.V2.Diagnostics;
internal class DiagnosticSummary(DateTime serverStartTime, IEnumerable<IDiagnosticEntry> entries, IdentityServerOptions options, ILoggerFactory loggerFactory)
internal class DiagnosticSummary(DiagnosticDataService diagnosticDataService, IdentityServerOptions options, ILoggerFactory loggerFactory)
{
private readonly ILogger _logger = loggerFactory.CreateLogger("Duende.IdentityServer.Diagnostics.Summary");
public async Task PrintSummary()
{
var bufferWriter = new ArrayBufferWriter<byte>();
await using var writer = new Utf8JsonWriter(bufferWriter, new JsonWriterOptions { Indented = false });
writer.WriteStartObject();
var diagnosticContext = new DiagnosticContext(serverStartTime, DateTime.UtcNow);
foreach (var diagnosticEntry in entries)
{
await diagnosticEntry.WriteAsync(diagnosticContext, writer);
}
writer.WriteEndObject();
await writer.FlushAsync();
var span = bufferWriter.WrittenSpan;
var jsonMemory = await diagnosticDataService.GetJsonBytesAsync();
var span = jsonMemory.Span;
using var diagnosticActivity = Tracing.DiagnosticsActivitySource.StartActivity("DiagnosticSummary");
var chunkSize = options.Diagnostics.ChunkSize;
@ -47,7 +32,7 @@ internal class DiagnosticSummary(DateTime serverStartTime, IEnumerable<IDiagnost
}
else
{
_logger.DiagnosticSummaryLogged(1, 1, Encoding.UTF8.GetString(bufferWriter.WrittenSpan));
_logger.DiagnosticSummaryLogged(1, 1, Encoding.UTF8.GetString(span));
}
}
}

View file

@ -0,0 +1,50 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
#nullable enable
using System.Buffers;
using System.Text;
using System.Text.Json;
using Duende.IdentityServer.Licensing.V2.Diagnostics;
namespace Duende.IdentityServer.Services;
public class DiagnosticDataService
{
private readonly DateTime _serverStartTime;
private readonly IEnumerable<IDiagnosticEntry> _entries;
internal DiagnosticDataService(DateTime serverStartTime, IEnumerable<IDiagnosticEntry> entries)
{
_serverStartTime = serverStartTime;
_entries = entries;
}
public async Task<ReadOnlyMemory<byte>> GetJsonBytesAsync()
{
var bufferWriter = new ArrayBufferWriter<byte>();
await using var writer = new Utf8JsonWriter(bufferWriter, new JsonWriterOptions { Indented = false });
writer.WriteStartObject();
var diagnosticContext = new DiagnosticContext(_serverStartTime, DateTime.UtcNow);
foreach (var diagnosticEntry in _entries)
{
await diagnosticEntry.WriteAsync(diagnosticContext, writer);
}
writer.WriteEndObject();
await writer.FlushAsync();
return bufferWriter.WrittenMemory;
}
public async Task<string> GetJsonStringAsync()
{
var bytes = await GetJsonBytesAsync();
return Encoding.UTF8.GetString(bytes.Span);
}
}

View file

@ -39,4 +39,28 @@ public class DynamicClientRegistrationTests : ConfigurationIntegrationTestBase
newClient.ClientSecrets.Count.ShouldBe(1);
newClient.ClientSecrets.Single().Value.ShouldBe(response.ClientSecret.Sha256());
}
[Fact]
public async Task request_for_public_client_does_not_require_client_secret()
{
IdentityServerHost.ApiScopes.Add(new ApiScope("api1"));
var request = new DynamicClientRegistrationRequest
{
RedirectUris = new[] { new Uri("https://example.com/callback") },
GrantTypes = new[] { "authorization_code" },
ClientName = "test",
ClientUri = new Uri("https://example.com"),
DefaultMaxAge = 10000,
Scope = "api1 openid profile",
TokenEndpointAuthenticationMethod = "none"
};
var httpResponse = await ConfigurationHost.HttpClient!.PostAsJsonAsync("/connect/dcr", request);
var response = await httpResponse.Content.ReadFromJsonAsync<DynamicClientRegistrationResponse>();
response.ShouldNotBeNull();
response.ClientSecret.ShouldBeNull();
response.RequireClientSecret.ShouldNotBeNull();
response.RequireClientSecret.Value.ShouldBeFalse();
}
}

View file

@ -113,4 +113,34 @@ public class DynamicClientRegistrationValidationTests : ConfigurationIntegration
var error = await response.Content.ReadFromJsonAsync<DynamicClientRegistrationError>();
error?.Error.ShouldBe("invalid_client_metadata");
}
[Fact]
public async Task client_credentials_and_do_not_require_client_secret_should_fail()
{
var response = await ConfigurationHost.HttpClient!.PostAsJsonAsync("/connect/dcr",
new DynamicClientRegistrationRequest
{
GrantTypes = { "client_credentials" },
RequireClientSecret = false
});
response.StatusCode.ShouldBe(HttpStatusCode.BadRequest);
var error = await response.Content.ReadFromJsonAsync<DynamicClientRegistrationError>();
error?.Error.ShouldBe("invalid_client_metadata");
}
[Fact]
public async Task client_credentials_and_token_endpoint_auth_method_none_should_fail()
{
var response = await ConfigurationHost.HttpClient!.PostAsJsonAsync("/connect/dcr",
new DynamicClientRegistrationRequest
{
GrantTypes = { "client_credentials" },
TokenEndpointAuthenticationMethod = "none"
});
response.StatusCode.ShouldBe(HttpStatusCode.BadRequest);
var error = await response.Content.ReadFromJsonAsync<DynamicClientRegistrationError>();
error?.Error.ShouldBe("invalid_client_metadata");
}
}

View file

@ -4,6 +4,7 @@
using System.Text.Json;
using Duende.IdentityServer.Configuration;
using Duende.IdentityServer.Licensing.V2.Diagnostics;
using Duende.IdentityServer.Services;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
@ -24,7 +25,8 @@ public class DiagnosticHostedServiceTests
secondDiagnosticEntry,
thirdDiagnosticEntry
};
var diagnosticSummary = new DiagnosticSummary(DateTime.UtcNow, entries, new IdentityServerOptions(), new StubLoggerFactory(diagnosticSummaryLogger));
var diagnosticService = new DiagnosticDataService(DateTime.UtcNow, entries);
var diagnosticSummary = new DiagnosticSummary(diagnosticService, new IdentityServerOptions(), new StubLoggerFactory(diagnosticSummaryLogger));
var options = Options.Create(new IdentityServerOptions());
var logger = new NullLogger<DiagnosticHostedService>();

View file

@ -5,6 +5,7 @@ using System.Text.Json;
using Duende.IdentityServer.Configuration;
using Duende.IdentityServer.Events;
using Duende.IdentityServer.Licensing.V2.Diagnostics;
using Duende.IdentityServer.Services;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging.Testing;
@ -25,7 +26,8 @@ public class DiagnosticSummaryTests
secondDiagnosticEntry,
thirdDiagnosticEntry
};
var summary = new DiagnosticSummary(DateTime.UtcNow, entries, new IdentityServerOptions(), new StubLoggerFactory(logger));
var diagnosticService = new DiagnosticDataService(DateTime.UtcNow, entries);
var summary = new DiagnosticSummary(diagnosticService, new IdentityServerOptions(), new StubLoggerFactory(logger));
await summary.PrintSummary();
@ -42,7 +44,8 @@ public class DiagnosticSummaryTests
var logger = new FakeLogger<DiagnosticSummary>();
var diagnosticEntry = new LongDiagnosticEntry { OutputLength = chunkSize * 2 };
var summary = new DiagnosticSummary(DateTime.UtcNow, [diagnosticEntry], options, new StubLoggerFactory(logger));
var diagnosticService = new DiagnosticDataService(DateTime.UtcNow, [diagnosticEntry]);
var summary = new DiagnosticSummary(diagnosticService, options, new StubLoggerFactory(logger));
await summary.PrintSummary();
@ -61,7 +64,9 @@ public class DiagnosticSummaryTests
var logger = new FakeLogger<DiagnosticSummary>();
var diagnosticEntry = new LongDiagnosticEntry { OutputLength = 2, OutputCharacter = '€' };
var summary = new DiagnosticSummary(DateTime.UtcNow, [diagnosticEntry], options, new StubLoggerFactory(logger));
var diagnosticService = new DiagnosticDataService(DateTime.UtcNow, [diagnosticEntry]);
var summary = new DiagnosticSummary(diagnosticService, options, new StubLoggerFactory(logger));
await summary.PrintSummary();
@ -76,7 +81,9 @@ public class DiagnosticSummaryTests
var logger = new FakeLogger<DiagnosticSummary>();
var diagnosticEntry = new LongDiagnosticEntry { OutputLength = options.Diagnostics.ChunkSize * 2 };
var summary = new DiagnosticSummary(DateTime.UtcNow, [diagnosticEntry], options, new StubLoggerFactory(logger));
var diagnosticService = new DiagnosticDataService(DateTime.UtcNow, [diagnosticEntry]);
var summary = new DiagnosticSummary(diagnosticService, options, new StubLoggerFactory(logger));
await summary.PrintSummary();
foreach (var entry in logger.Collector.GetSnapshot())
@ -91,7 +98,8 @@ public class DiagnosticSummaryTests
var options = new IdentityServerOptions();
var logger = new FakeLogger<DiagnosticSummary>();
var diagnosticEntry = new LongDiagnosticEntry { OutputLength = 100000 };
var summary = new DiagnosticSummary(DateTime.UtcNow, [diagnosticEntry], options, new StubLoggerFactory(logger));
var diagnosticService = new DiagnosticDataService(DateTime.UtcNow, [diagnosticEntry]);
var summary = new DiagnosticSummary(diagnosticService, options, new StubLoggerFactory(logger));
await summary.PrintSummary();

View file

@ -0,0 +1,272 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using System.Text;
using System.Text.Json;
using Duende.IdentityServer.Licensing.V2.Diagnostics;
using Duende.IdentityServer.Services;
namespace IdentityServer.UnitTests.Services;
public class DiagnosticDataServiceTests
{
[Fact]
public async Task GetJsonBytesAsync_WithNoEntries_ShouldReturnEmptyJsonObject()
{
var serverStartTime = DateTime.UtcNow.AddMinutes(-5);
var entries = new List<IDiagnosticEntry>();
var service = new DiagnosticDataService(serverStartTime, entries);
var result = await service.GetJsonBytesAsync();
var json = Encoding.UTF8.GetString(result.Span);
json.ShouldBe("{}");
}
[Fact]
public async Task GetJsonBytesAsync_WithSingleEntry_ShouldReturnValidJson()
{
var serverStartTime = DateTime.UtcNow.AddMinutes(-5);
var entries = new List<IDiagnosticEntry>
{
new TestDiagnosticEntry("TestProperty", "TestValue")
};
var service = new DiagnosticDataService(serverStartTime, entries);
var result = await service.GetJsonBytesAsync();
var json = Encoding.UTF8.GetString(result.Span);
var jsonDoc = JsonDocument.Parse(json);
jsonDoc.RootElement.GetProperty("TestProperty").GetString().ShouldBe("TestValue");
}
[Fact]
public async Task GetJsonBytesAsync_WithMultipleEntries_ShouldIncludeAllEntries()
{
var serverStartTime = DateTime.UtcNow.AddMinutes(-5);
var entries = new List<IDiagnosticEntry>
{
new TestDiagnosticEntry("Property1", "Value1"),
new TestDiagnosticEntry("Property2", "Value2"),
new TestDiagnosticEntry("Property3", "Value3")
};
var service = new DiagnosticDataService(serverStartTime, entries);
var result = await service.GetJsonBytesAsync();
var json = Encoding.UTF8.GetString(result.Span);
var jsonDoc = JsonDocument.Parse(json);
jsonDoc.RootElement.GetProperty("Property1").GetString().ShouldBe("Value1");
jsonDoc.RootElement.GetProperty("Property2").GetString().ShouldBe("Value2");
jsonDoc.RootElement.GetProperty("Property3").GetString().ShouldBe("Value3");
}
[Fact]
public async Task GetJsonBytesAsync_ShouldPassCorrectDiagnosticContext()
{
var serverStartTime = DateTime.UtcNow.AddMinutes(-5);
var capturedContext = new TestDiagnosticEntry.ContextCapture();
var entries = new List<IDiagnosticEntry>
{
new TestDiagnosticEntry("TestProperty", "TestValue", capturedContext)
};
var service = new DiagnosticDataService(serverStartTime, entries);
await service.GetJsonBytesAsync();
capturedContext.Context.ShouldNotBeNull();
capturedContext.Context.ServerStartTime.ShouldBe(serverStartTime);
capturedContext.Context.CurrentSeverTime.ShouldBeGreaterThanOrEqualTo(serverStartTime);
}
[Fact]
public async Task GetJsonBytesAsync_ShouldProduceCompactJson()
{
var serverStartTime = DateTime.UtcNow.AddMinutes(-5);
var entries = new List<IDiagnosticEntry>
{
new TestDiagnosticEntry("Property1", "Value1"),
new TestDiagnosticEntry("Property2", "Value2")
};
var service = new DiagnosticDataService(serverStartTime, entries);
var result = await service.GetJsonBytesAsync();
var json = Encoding.UTF8.GetString(result.Span);
json.ShouldNotContain("\n");
json.ShouldNotContain("\r");
json.ShouldNotContain(" ");
}
[Fact]
public async Task GetJsonStringAsync_WithNoEntries_ShouldReturnEmptyJsonObject()
{
var serverStartTime = DateTime.UtcNow.AddMinutes(-5);
var entries = new List<IDiagnosticEntry>();
var service = new DiagnosticDataService(serverStartTime, entries);
var result = await service.GetJsonStringAsync();
result.ShouldBe("{}");
}
[Fact]
public async Task GetJsonStringAsync_WithSingleEntry_ShouldReturnValidJson()
{
var serverStartTime = DateTime.UtcNow.AddMinutes(-5);
var entries = new List<IDiagnosticEntry>
{
new TestDiagnosticEntry("TestProperty", "TestValue")
};
var service = new DiagnosticDataService(serverStartTime, entries);
var result = await service.GetJsonStringAsync();
var jsonDoc = JsonDocument.Parse(result);
jsonDoc.RootElement.GetProperty("TestProperty").GetString().ShouldBe("TestValue");
}
[Fact]
public async Task GetJsonStringAsync_WithMultipleEntries_ShouldIncludeAllEntries()
{
var serverStartTime = DateTime.UtcNow.AddMinutes(-5);
var entries = new List<IDiagnosticEntry>
{
new TestDiagnosticEntry("Property1", "Value1"),
new TestDiagnosticEntry("Property2", "Value2"),
new TestDiagnosticEntry("Property3", "Value3")
};
var service = new DiagnosticDataService(serverStartTime, entries);
var result = await service.GetJsonStringAsync();
var jsonDoc = JsonDocument.Parse(result);
jsonDoc.RootElement.GetProperty("Property1").GetString().ShouldBe("Value1");
jsonDoc.RootElement.GetProperty("Property2").GetString().ShouldBe("Value2");
jsonDoc.RootElement.GetProperty("Property3").GetString().ShouldBe("Value3");
}
[Fact]
public async Task GetJsonStringAsync_ShouldReturnUtf8EncodedString()
{
var serverStartTime = DateTime.UtcNow.AddMinutes(-5);
var entries = new List<IDiagnosticEntry>
{
new TestDiagnosticEntry("Property", "Value with émojis 🎉")
};
var service = new DiagnosticDataService(serverStartTime, entries);
var result = await service.GetJsonStringAsync();
var jsonDoc = JsonDocument.Parse(result);
jsonDoc.RootElement.GetProperty("Property").GetString().ShouldBe("Value with émojis 🎉");
}
[Fact]
public async Task GetJsonStringAsync_ShouldMatchGetJsonBytesAsync()
{
var serverStartTime = DateTime.UtcNow.AddMinutes(-5);
var entries = new List<IDiagnosticEntry>
{
new TestDiagnosticEntry("Property1", "Value1"),
new TestDiagnosticEntry("Property2", "Value2")
};
var service = new DiagnosticDataService(serverStartTime, entries);
var stringResult = await service.GetJsonStringAsync();
var bytesResult = await service.GetJsonBytesAsync();
var stringFromBytes = Encoding.UTF8.GetString(bytesResult.Span);
stringResult.ShouldBe(stringFromBytes);
}
[Fact]
public async Task GetJsonBytesAsync_WithComplexEntry_ShouldWriteNestedObjects()
{
var serverStartTime = DateTime.UtcNow.AddMinutes(-5);
var entries = new List<IDiagnosticEntry>
{
new ComplexTestDiagnosticEntry()
};
var service = new DiagnosticDataService(serverStartTime, entries);
var result = await service.GetJsonBytesAsync();
var json = Encoding.UTF8.GetString(result.Span);
var jsonDoc = JsonDocument.Parse(json);
var complex = jsonDoc.RootElement.GetProperty("ComplexData");
complex.GetProperty("StringValue").GetString().ShouldBe("test");
complex.GetProperty("NumberValue").GetInt32().ShouldBe(42);
complex.GetProperty("BoolValue").GetBoolean().ShouldBeTrue();
}
[Fact]
public async Task GetJsonBytesAsync_WithAsyncEntry_ShouldHandleAsyncWrites()
{
var serverStartTime = DateTime.UtcNow.AddMinutes(-5);
var entries = new List<IDiagnosticEntry>
{
new AsyncTestDiagnosticEntry()
};
var service = new DiagnosticDataService(serverStartTime, entries);
var result = await service.GetJsonBytesAsync();
var json = Encoding.UTF8.GetString(result.Span);
var jsonDoc = JsonDocument.Parse(json);
jsonDoc.RootElement.GetProperty("AsyncData").GetString().ShouldBe("async value");
}
// Test helper classes
private class TestDiagnosticEntry : IDiagnosticEntry
{
private readonly string _propertyName;
private readonly string _propertyValue;
private readonly ContextCapture _contextCapture;
public TestDiagnosticEntry(string propertyName, string propertyValue, ContextCapture contextCapture = null)
{
_propertyName = propertyName;
_propertyValue = propertyValue;
_contextCapture = contextCapture;
}
public Task WriteAsync(DiagnosticContext context, Utf8JsonWriter writer)
{
if (_contextCapture != null)
{
_contextCapture.Context = context;
}
writer.WriteString(_propertyName, _propertyValue);
return Task.CompletedTask;
}
public class ContextCapture
{
public DiagnosticContext Context { get; set; }
}
}
private class ComplexTestDiagnosticEntry : IDiagnosticEntry
{
public Task WriteAsync(DiagnosticContext context, Utf8JsonWriter writer)
{
writer.WritePropertyName("ComplexData");
writer.WriteStartObject();
writer.WriteString("StringValue", "test");
writer.WriteNumber("NumberValue", 42);
writer.WriteBoolean("BoolValue", true);
writer.WriteEndObject();
return Task.CompletedTask;
}
}
private class AsyncTestDiagnosticEntry : IDiagnosticEntry
{
public async Task WriteAsync(DiagnosticContext context, Utf8JsonWriter writer)
{
await Task.Delay(1);
writer.WriteString("AsyncData", "async value");
}
}
}