Winche.KeycloakClient 2.0.0

dotnet add package Winche.KeycloakClient --version 2.0.0
                    
NuGet\Install-Package Winche.KeycloakClient -Version 2.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Winche.KeycloakClient" Version="2.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Winche.KeycloakClient" Version="2.0.0" />
                    
Directory.Packages.props
<PackageReference Include="Winche.KeycloakClient" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Winche.KeycloakClient --version 2.0.0
                    
#r "nuget: Winche.KeycloakClient, 2.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Winche.KeycloakClient@2.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Winche.KeycloakClient&version=2.0.0
                    
Install as a Cake Addin
#tool nuget:?package=Winche.KeycloakClient&version=2.0.0
                    
Install as a Cake Tool

KeycloakClient

An opinionated ASP.NET Core integration for the Keycloak Admin API and the Phase Two webhook plugin. Configure once via appsettings.json, then inject IKeycloakClientService and write event handlers.

Features

  • Service-account IKeycloakClientService — backed by client-credentials token management (Duende.AccessTokenManagement). Works for background jobs and unauthenticated callers.
  • Optional delegated IKeycloakClientService — forwards the incoming request's bearer token to the Admin API. Operations run with the caller's privileges.
  • Admin API coverage — users (CRUD, search, count, reset-password, action emails), groups, user/group membership.
  • Webhook intake — app.UseKeycloakWebHooks() mounts an endpoint, validates the shared secret in constant time, and dispatches typed events to your handlers.
  • Automatic registration — registers (or removes) the webhook on startup based on appsettings.json.

Requirements

  • .NET 10 (net10.0)
  • ASP.NET Core 10
  • A Keycloak realm with a confidential client (for the service-account flow)
  • The Phase Two webhook plugin installed in Keycloak (only if you use webhook features)

Installation

dotnet add package Winche.KeycloakClient

Configuration

Add a Keycloak section to appsettings.json:

{
  "Keycloak": {
    "Server": "https://id.example.com",
    "Realm": "myrealm",
    "Resource": "myapp",
    "Agent": "MyApp/1.0",
    "Credentials": {
      "Secret": "REPLACE_ME"
    },
    "Webhook": {
      "Enabled": true,
      "URL": "https://myapp.example.com/webhooks/keycloak",
      "Secret": "shared-secret-with-keycloak"
    }
  }
}
Key Required Description
Server yes Keycloak base URL (no trailing slash).
Realm yes Target realm.
Resource yes Confidential client id used for the service-account flow.
Credentials.Secret yes (service-account flow) Client secret.
Agent no User-Agent header sent on outgoing requests.
Webhook.Enabled no true to register, false to remove. Default true.
Webhook.URL yes (webhooks) Publicly reachable URL Keycloak will POST events to.
Webhook.Secret no Shared secret. When set, incoming events must carry header X-Webhook-Secret: <value>.

Authentication & authorization

This package also configures ASP.NET Core JWT bearer authentication and Keycloak-aware authorization (role policies + UMA / Authorization Services) when you call the dedicated extensions.

appsettings.json

Both sections are optional; add only what you use.

{
  "Keycloak": {
    "Server": "https://id.example.com",
    "Realm": "myrealm",
    "Resource": "myapp",
    "Credentials": { "Secret": "REPLACE_ME" },

    "Authentication": {
      "ValidateAudience": true,
      "RolesSource": "RealmAndResource",
      "RealmRolePrefix": null,
      "ResourceRolePrefix": null,
      "AdditionalResourceClients": []
    },

    "Authorization": {
      "CacheDuration": null,
      "DecisionStrategy": "Unanimous"
    }
  }
}
Key Default Meaning
Authentication.ValidateAudience true Strict aud contains Resource validation — this is what enforces "is this token for me?". Keep on for any user-bearer-token flow (SPA → API, mobile → API); the standard Keycloak setup requires adding an Audience mapper so aud includes your client id. Set to false only if you have a deliberate reason (e.g. internal-only audience inference). The azp claim is not checked — per OIDC, azp identifies the requesting client and is not an access-control claim.
Authentication.RolesSource RealmAndResource Which Keycloak role sources to flatten into ClaimTypes.Role: Realm, Resource, or both.
Authentication.RealmRolePrefix / ResourceRolePrefix null Optional prefix prepended to each role claim, e.g. "realm:" produces realm:admin.
Authentication.AdditionalResourceClients [] Extra resource_access.* entries to read roles from, beyond the configured Resource.
Authorization.CacheDuration null (off) If set, UMA decisions are cached in IDistributedCache for the given TimeSpan. Cache key is kc:authz:{sha256(bearer)}:{resource}:{scope}.
Authorization.DecisionStrategy Unanimous Reserved for future client-side composition; bound from config but not transmitted to Keycloak in v1.

Registration

using Winche.KeycloakClient.DependencyInjection;

builder.Services
    .AddKeycloakClient(builder.Configuration)
    .AddKeycloakAuthentication(builder.Configuration)
    .AddKeycloakAuthorization(builder.Configuration, c => c
        .AddRealmRolePolicy("admin")
        .AddResourceRolePolicy("editor")
        .AddProtectedResourcePolicy("can-read-doc", "document", "read"));

app.UseAuthentication();
app.UseAuthorization();

Usage

Role-based — [Authorize] with realm/resource roles:

app.MapGet("/admin/users", () => "ok").RequireAuthorization("admin");
app.MapGet("/editor/doc", () => "ok").RequireAuthorization("editor");

After authentication, both realm and resource roles are exposed as ClaimTypes.Role claims, so [Authorize(Roles = "admin")] and User.IsInRole("admin") also work.

Original OIDC claim names are preserved on ClaimsPrincipal — User.FindFirstValue("sub"), User.FindFirstValue("email"), User.FindFirstValue("preferred_username") work directly. The JwtBearer handler's default inbound-claim remapping (which would rewrite sub to …/nameidentifier, email to …/emailaddress, etc.) is disabled.

Endpoint-level UMA — [ProtectedResource] attribute or kc:protected:* policy:

app.MapGet("/documents", [ProtectedResource("document", "read")] () => "ok");

// Equivalent:
app.MapGet("/documents", () => "ok")
    .RequireAuthorization("kc:protected:document:read");

Both forms cause KeycloakProtectedResourceHandler to ask Keycloak whether the current token is granted read on document. A denial returns 403.

Resource-instance UMA — imperative IKeycloakAuthorizationService:

app.MapGet("/documents/{id:int}", async (
        int id,
        IKeycloakAuthorizationService authz,
        IDocumentStore store,
        CancellationToken ct) =>
{
    await authz.RequireAsync($"document:{id}", "read", ct);
    return Results.Ok(await store.GetAsync(id, ct));
}).RequireAuthorization();

RequireAsync throws KeycloakAuthorizationException on deny; let it bubble and exception-handling middleware can translate to 403. Use AuthorizeAsync (returns bool) when you need to branch on the decision.

Quick start

Registration

using Winche.KeycloakClient.DependencyInjection;

const string DelegatedClientKey = "user";

builder.Services
    .AddKeycloakClient(builder.Configuration, c => c.AddDelegatedClient(DelegatedClientKey))
    .AddKeycloakWebHooks(c => c.AddEventHandler<MyUserCreatedHandler>());

AddDelegatedClient is optional — only register it if you want token-forwarding endpoints.

Service-account client

Resolves the default IKeycloakClientService. Uses the configured client credentials for every call — no caller context needed.

app.MapGet("/users", async (IKeycloakClientService client, CancellationToken ct) =>
    Results.Ok(await client.GetUsersAsync(cancellationToken: ct)));

Delegated client

Resolves the keyed IKeycloakClientService registered via AddDelegatedClient(key). The handler reads the incoming Authorization: Bearer <token> header and forwards it. Throws InvalidOperationException if called outside of an HTTP request scope or if the request has no bearer token — fail fast rather than silently issuing unauthorized requests.

app.MapGet("/me/users", async (
        [FromKeyedServices("user")] IKeycloakClientService client,
        CancellationToken ct) =>
    Results.Ok(await client.GetUsersAsync(cancellationToken: ct)))
    .RequireAuthorization();

The forwarded token must carry the right realm-management roles for the operation to succeed (Keycloak enforces this server-side).

Webhook endpoint

app.UseKeycloakWebHooks(); // mounts POST /webhooks/keycloak by default

Pass a different path if you want: app.UseKeycloakWebHooks("/hooks/kc");. The endpoint validates X-Webhook-Secret against Keycloak:Webhook:Secret using a constant-time comparison.

Event handlers

Derive from KeycloakEventHandler, declare the event types you care about, and implement HandleAsync:

using Winche.KeycloakClient.Abstraction;
using Winche.KeycloakClient.Models;

public sealed class UserCreatedHandler(ILogger<UserCreatedHandler> logger) : KeycloakEventHandler
{
    public override IReadOnlySet<string> Types { get; } = new HashSet<string>
    {
        "access.REGISTER",
        "admin.USER-CREATE"
    };

    public override Task HandleAsync(KeycloakWebhookEvent @event, CancellationToken ct)
    {
        logger.LogInformation("Got '{Type}' for {Username}", @event.Type, @event.Representation?.Username);
        return Task.CompletedTask;
    }
}

Register the handler when wiring webhooks: c.AddEventHandler<UserCreatedHandler>(). Handlers are invoked in parallel; exceptions from one handler don't affect others.

Webhook lifecycle

On startup, the library:

  1. Reads Keycloak:Webhook from configuration.
  2. If Url is blank → skip.
  3. Otherwise, find any existing Keycloak webhook with the same Url and delete it.
  4. If Enabled = true → create a fresh webhook with the configured URL, secret, and the union of all Types from registered handlers.
  5. If Enabled = false → stop after step 3 (a configuration toggle that also tidies up upstream).

Always delete-and-recreate keeps the secret in sync with appsettings.json, since Keycloak's webhook GET endpoint does not return secrets.

License

MIT

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.0.0 125 8/20/2026
1.2.1 174 5/15/2026
1.2.0 116 5/14/2026
1.1.1 113 5/14/2026
1.1.0 118 5/14/2026
1.0.0 125 5/14/2026