Update Console Clients & Aspire version (#2270)

* Updated console client configuration for Windows System Browser console client.
Updated Windows System Console so default URL behaviour and callback works.
Updated Aspire to v13.

* Remove an unused using

* Refactored to fix a number of issues and introduce improvements using Spectre Console.

* Updated Aspire Test package
This commit is contained in:
Stu Frankish 2025-11-13 10:57:00 +00:00 committed by GitHub
parent ad86e49413
commit 5fa0ca61b2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 572 additions and 127 deletions

View file

@ -30,9 +30,9 @@ that supports the target frameworks our products target (8, 9, 10) -->
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="AngleSharp" Version="1.1.2" />
<PackageVersion Include="Aspire.Hosting.AppHost" Version="9.5.2" />
<PackageVersion Include="Aspire.Hosting.Testing" Version="9.5.2" />
<PackageVersion Include="Aspire.Hosting.SqlServer" Version="9.5.2" />
<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" />
@ -125,6 +125,8 @@ that supports the target frameworks our products target (8, 9, 10) -->
<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="10.0.0" />

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

@ -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,