---
title: Keycloak in Aspire for local development
date: 2026-06-04
tags: dotnet, aspire, keycloak, blazor, auth
summary: Run Keycloak next to your app with Aspire, import a realm, and log in from Blazor. Plus the claim mapping trap.
---

In one of my projects the customer runs their own identity provider, and I can't use it for development. So I mock it with Keycloak. Aspire has a hosting package for this (still in preview when I wrote it), and it is only used locally.

## AppHost

Install the NuGet package `Aspire.Hosting.Keycloak` and add Keycloak to the AppHost:

```csharp title="AppHost.cs"
var keycloak = builder.AddKeycloak("keycloak", 5533)
    .WithDataVolume()
    .WithRealmImport("realm-export.json");
```

This starts the [`quay.io/keycloak/keycloak`](https://quay.io/repository/keycloak/keycloak) container.

- `WithDataVolume()` keeps the data when the container is recreated.
- `WithRealmImport()` loads a realm from a JSON file.

The second one is the nice part. In Keycloak you can export a realm as JSON. Check that file in, and everyone gets the same realm, roles, app settings and test users on the first start.

## Blazor app

My app is a Blazor Server project, and I had to switch authentication from Entra ID to Keycloak. With OpenID Connect it looks like this:

```csharp title="Program.cs"
builder.Services.AddAuthentication(options =>
    {
        options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
        options.DefaultSignOutScheme = OpenIdConnectDefaults.AuthenticationScheme;
    })
    .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddOpenIdConnect(options =>
    {
        builder.Configuration.GetSection("Keycloak").Bind(options);
        options.RequireHttpsMetadata = !builder.Environment.IsDevelopment();
        options.ResponseType = OpenIdConnectResponseType.Code;
        options.GetClaimsFromUserInfoEndpoint = true;
        options.SaveTokens = true;
        options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.TokenValidationParameters = new TokenValidationParameters
        {
            NameClaimType = "name",
            RoleClaimType = "role"
        };
        // Keep the claim names as Keycloak sends them
        options.MapInboundClaims = false;
    });
```

## The trap: MapInboundClaims

Look at the last line. If you leave `MapInboundClaims` at its default (`true`), .NET renames incoming claims to the old Microsoft schema. The role claim becomes `http://schemas.microsoft.com/ws/2008/06/identity/claims/role`, and a check like this quietly stops working:

```csharp title="role check"
context.User.IsInRole(AdminRequirement.RoleName)
```

Set it to `false` and the role claim keeps its name.

One more thing: by default Keycloak doesn't send a flat `role` claim at all. It puts the roles into `realm_access.roles`. Add a protocol mapper to the client (a "User Realm Role" mapper, multivalued, token claim name `role`, added to the ID token and userinfo), so `RoleClaimType = "role"` has something to read. Put it in the realm export and everyone gets it.

> TIL: with Keycloak roles, set `MapInboundClaims = false`, or `IsInRole` quietly returns false.

## JWT bearer instead

If you have an API and not a web app with a login page, you can use JWT bearer authentication instead of OpenID Connect. The Microsoft docs cover both: [Aspire Keycloak integration](https://learn.microsoft.com/en-us/dotnet/aspire/authentication/keycloak-integration?tabs=dotnet-cli).
