This post shows how to implement an ASP.NET Core application which uses OpenID Connect and OAuth PAR for authentication. The client application uses Keycloak as the identity provider. The Keycloak application is hosted in a docker container. The applications are run locally using .NET Aspire. This makes it really easy to develop using containers.
Code: https://github.com/damienbod/keycloak-backchannel
Setup
The standard Aspire Microsoft template was used to setup the .NET Aspire AppHost, ServiceDefaults projects. The Keycloak container service was added to the AppHost project using the Keycloak.AuthServices.Aspire.Hosting Nuget package. An ASP.NET Core Razor Page project was added as the UI client, but any project can be used like Blazor or an MVC application.

Keycloak Setup
The Keycloak Container is completely setup in the AppHost project. The Keycloak.AuthServices.Aspire.Hosting Nuget package is used to add the integration to .NET Aspire. For this to work, Docker Desktop needs to be installed in the development environment. I want to use the Keycloak preview features and initialized this using the WithArgs method. If using the Microsoft Keycloak package, the setup is almost identical.
var userName = builder.AddParameter("userName");
var password = builder.AddParameter("password", secret: true);
var keycloak = builder.AddKeycloakContainer("keycloak",
userName: userName, password: password, port: 8080)
.WithArgs("--features=preview")
.WithDataVolume()
.RunWithHttpsDevCertificate(port: 8081);
I want to develop using HTTPS and so the Keycloak container needs to run in HTTPS as well. This was not so simple to setup, but Damien Edwards provided a solution which works great.
The RunWithHttpsDevCertificate extension method was added using his code and adapted so that the port is fixed for the HTTPS Keycloak server. This implementation requires the System.IO.Hashing Nuget package.
using System.Diagnostics;
using System.IO.Hashing;
using System.Text;
namespace Aspire.Hosting;
/// <summary>
/// Original src code:
/// https://github.com/dotnet/aspire-samples/blob/b741f5e78a86539bc9ab12cd7f4a5afea7aa54c4/samples/Keycloak/Keycloak.AppHost/HostingExtensions.cs
/// </summary>
public static class HostingExtensions
{
/// <summary>
/// Injects the ASP.NET Core HTTPS developer certificate into the resource via the specified environment variables when
/// <paramref name="builder"/>.<see cref="IResourceBuilder{T}.ApplicationBuilder">ApplicationBuilder</see>.
/// <see cref="IDistributedApplicationBuilder.ExecutionContext">ExecutionContext</see>.<see cref="DistributedApplicationExecutionContext.IsRunMode">IsRunMode</see><c> == true</c>.<br/>
/// If the resource is a <see cref="ContainerResource"/>, the certificate files will be bind mounted into the container.
/// </summary>
/// <remarks>
/// This method <strong>does not</strong> configure an HTTPS endpoint on the resource. Use <see cref="ResourceBuilderExtensions.WithHttpsEndpoint{TResource}"/> to configure an HTTPS endpoint.
/// </remarks>
public static IResourceBuilder<TResource> RunWithHttpsDevCertificate<TResource>(this IResourceBuilder<TResource> builder, string certFileEnv, string certKeyFileEnv)
where TResource : IResourceWithEnvironment
{
const string DEV_CERT_DIR = "/dev-certs";
if (builder.ApplicationBuilder.ExecutionContext.IsRunMode)
{
// Export the ASP.NET Core HTTPS development certificate & private key to PEM files, bind mount them into the container
// and configure it to use them via the specified environment variables.
var (certPath, _) = ExportDevCertificate(builder.ApplicationBuilder);
var bindSource = Path.GetDirectoryName(certPath) ?? throw new UnreachableException();
if (builder.Resource is ContainerResource containerResource)
{
builder.ApplicationBuilder.CreateResourceBuilder(containerResource)
.WithBindMount(bindSource, DEV_CERT_DIR, isReadOnly: true);
}
builder
.WithEnvironment(certFileEnv, $"{DEV_CERT_DIR}/dev-cert.pem")
.WithEnvironment(certKeyFileEnv, $"{DEV_CERT_DIR}/dev-cert.key");
}
return builder;
}
/// <summary>
/// Configures the Keycloak container to use the ASP.NET Core HTTPS development certificate created by <c>dotnet dev-certs</c> when
/// <paramref name="builder"/><c>.ExecutionContext.IsRunMode == true</c>.
/// </summary>
/// <remarks>
/// See <see href="https://learn.microsoft.com/dotnet/core/tools/dotnet-dev-certs">https://learn.microsoft.com/dotnet/core/tools/dotnet-dev-certs</see>
/// for more information on the <c>dotnet dev-certs</c> tool.<br/>
/// See <see href="https://learn.microsoft.com/aspnet/core/security/enforcing-ssl#trust-the-aspnet-core-https-development-certificate-on-windows-and-macos">
/// https://learn.microsoft.com/aspnet/core/security/enforcing-ssl</see>
/// for more information on the ASP.NET Core HTTPS development certificate.
/// </remarks>
public static IResourceBuilder<KeycloakResource> RunWithHttpsDevCertificate(this IResourceBuilder<KeycloakResource> builder, int port = 8081, int targetPort = 8443)
{
if (builder.ApplicationBuilder.ExecutionContext.IsRunMode)
{
// Mount the ASP.NET Core HTTPS development certificate in the Keycloak container and configure Keycloak to it
// via the KC_HTTPS_CERTIFICATE_FILE and KC_HTTPS_CERTIFICATE_KEY_FILE environment variables.
builder
.RunWithHttpsDevCertificate("KC_HTTPS_CERTIFICATE_FILE", "KC_HTTPS_CERTIFICATE_KEY_FILE")
.WithHttpsEndpoint(port: port, targetPort: targetPort)
.WithEnvironment("KC_HOSTNAME", "localhost")
// Without disabling HTTP/2 you can hit HTTP 431 Header too large errors in Keycloak.
// Related issues:
// https://github.com/keycloak/keycloak/discussions/10236
// https://github.com/keycloak/keycloak/issues/13933
// https://github.com/quarkusio/quarkus/issues/33692
.WithEnvironment("QUARKUS_HTTP_HTTP2", "false");
}
return builder;
}
private static (string, string) ExportDevCertificate(IDistributedApplicationBuilder builder)
{
// Exports the ASP.NET Core HTTPS development certificate & private key to PEM files using 'dotnet dev-certs https' to a temporary
// directory and returns the path.
// TODO: Check if we're running on a platform that already has the cert and key exported to a file (e.g. macOS) and just use those instead.
var appNameHashBytes = XxHash64.Hash(Encoding.Unicode.GetBytes(builder.Environment.ApplicationName).AsSpan());
var appNameHash = BitConverter.ToString(appNameHashBytes).Replace("-", "").ToLowerInvariant();
var tempDir = Path.Combine(Path.GetTempPath(), $"aspire.{appNameHash}");
var certExportPath = Path.Combine(tempDir, "dev-cert.pem");
var certKeyExportPath = Path.Combine(tempDir, "dev-cert.key");
if (File.Exists(certExportPath) && File.Exists(certKeyExportPath))
{
// Certificate already exported, return the path.
return (certExportPath, certKeyExportPath);
}
else if (Directory.Exists(tempDir))
{
Directory.Delete(tempDir, recursive: true);
}
var exportProcess = Process.Start("dotnet", $"dev-certs https --export-path \"{certExportPath}\" --format Pem --no-password");
var exited = exportProcess.WaitForExit(TimeSpan.FromSeconds(5));
if (exited && File.Exists(certExportPath) && File.Exists(certKeyExportPath))
{
return (certExportPath, certKeyExportPath);
}
else if (exportProcess.HasExited && exportProcess.ExitCode != 0)
{
throw new InvalidOperationException($"HTTPS dev certificate export failed with exit code {exportProcess.ExitCode}");
}
else if (!exportProcess.HasExited)
{
exportProcess.Kill(true);
throw new InvalidOperationException("HTTPS dev certificate export timed out");
}
throw new InvalidOperationException("HTTPS dev certificate export failed for an unknown reason");
}
}
Note: The AppHost project must reference all the services used in the solution.
Keycloak client configuration
See the razorpagepar.json file in the git repository. This is a Keycloak export of the whole client. This can be imported and updated.
The client is configured to use PAR.

ASP.NET Core OpenID Connect client using OAuth PAR
The client application uses the standard OpenID Connect client and requires OAuth PAR for authentication. This is a new feature in .NET 9. The repo has a Razor Page OpenID Connect example as well as an MVC client sample. This would be the same for a Blazor application.
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.Authority = authConfiguration["StsServerIdentityUrl"];
options.ClientSecret = authConfiguration["ClientSecret"];
options.ClientId = authConfiguration["Audience"];
options.ResponseType = "code";
options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("email");
options.Scope.Add("offline_access");
options.ClaimActions.Remove("amr");
options.ClaimActions.MapJsonKey("website", "website");
options.GetClaimsFromUserInfoEndpoint = true;
options.SaveTokens = true;
options.PushedAuthorizationBehavior = PushedAuthorizationBehavior.Require;
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = JwtClaimTypes.Name,
RoleClaimType = JwtClaimTypes.Role,
};
});
Notes
.NET Aspire looks great and is easy to use in development. I am only learning this and must learn the details now. I have some issues using the containers and HTTPS and I don’t understand how the configuration works. I also don’t understand how this would work in production. Lots to learn.
Links
https://www.keycloak.org/server/features
https://github.com/NikiforovAll/keycloak-authorization-services-dotnet
https://openid.net/specs/openid-connect-backchannel-1_0.html
https://github.com/dotnet/aspire-samples/tree/main/samples
https://learn.microsoft.com/en-us/dotnet/aspire/get-started/aspire-overview