LowCodeHub.Keycloak.Client 0.0.3

dotnet add package LowCodeHub.Keycloak.Client --version 0.0.3
                    
NuGet\Install-Package LowCodeHub.Keycloak.Client -Version 0.0.3
                    
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="LowCodeHub.Keycloak.Client" Version="0.0.3" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="LowCodeHub.Keycloak.Client" Version="0.0.3" />
                    
Directory.Packages.props
<PackageReference Include="LowCodeHub.Keycloak.Client" />
                    
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 LowCodeHub.Keycloak.Client --version 0.0.3
                    
#r "nuget: LowCodeHub.Keycloak.Client, 0.0.3"
                    
#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 LowCodeHub.Keycloak.Client@0.0.3
                    
#: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=LowCodeHub.Keycloak.Client&version=0.0.3
                    
Install as a Cake Addin
#tool nuget:?package=LowCodeHub.Keycloak.Client&version=0.0.3
                    
Install as a Cake Tool

LowCodeHub.Keycloak.Client

A typed Keycloak client SDK for .NET. Full CRUD for users, groups, roles, sessions, clients, federated identities, and authentication — with automatic admin token management, stampede-safe caching, and built-in HTTP resilience.

NuGet License: MIT

Why This Library?

Feature LowCodeHub.Keycloak.Client Raw Keycloak REST API Other Keycloak SDKs
Admin token management Automatic — cached, stampede-safe, refreshed before expiry Manual token lifecycle Varies
API surface Facade patternkeycloak.Users.…, keycloak.Groups.… 200+ raw endpoints Flat god-interface
User CRUD Typed methods — create, update, delete, search, attributes Raw HTTP calls Partial
Group management Full — CRUD, membership, sub-groups, count Raw HTTP calls Partial
Role assignment Realm + client roles — assign, remove, effective, available Raw HTTP calls Partial
Session management List, logout, delete Raw HTTP calls Rarely supported
User authentication Login, refresh, logout, userinfo, introspect Raw HTTP calls Varies
Federated identities Get, link, unlink Raw HTTP calls Rarely supported
Impersonation One method Raw HTTP calls Rarely supported
Client secrets Get + regenerate Raw HTTP calls Rarely supported
HTTP resilience Built-in — retries, circuit breakers, timeouts Manual Manual
DI integration One extension methodIValidateOptions + startup validation Manual HttpClient Varies

Installation

dotnet add package LowCodeHub.Keycloak.Client

Quick Start

using LowCodeHub.Keycloak.Client.Extensions;

builder.Services.AddKeycloakClient(options =>
{
    options.KeycloakBaseUrl = "https://keycloak.example.com";
    options.Realm = "my-realm";
    options.Credentials = new()
    {
        ClientId = "admin-cli",
        ClientSecret = "your-client-secret"
    };
});
public class UserService(IKeycloakClient keycloak)
{
    public Task<KeycloakUser?> GetByEmailAsync(string email, CancellationToken ct)
        => keycloak.Users.GetByEmailAsync(email, ct);

    public Task<string> CreateAsync(string username, string email, CancellationToken ct)
        => keycloak.Users.CreateAsync(new CreateUserRequest
        {
            Username = username,
            Email = email,
            Enabled = true
        }, ct);

    public Task<KeycloakToken> LoginAsync(string username, string password, CancellationToken ct)
        => keycloak.Authentication.LoginAsync(new LoginRequest
        {
            Username = username,
            Password = password
        }, ct);
}

Inject IKeycloakClient and access resource-scoped sub-clients: .Users, .Groups, .Roles, .Clients, .Sessions, .Authentication, .Impersonation, .FederatedIdentities.


Table of Contents


Configuration

Code-Based Configuration

builder.Services.AddKeycloakClient(options =>
{
    options.KeycloakBaseUrl = "https://keycloak.example.com";
    options.Realm = "my-realm";
    options.Credentials = new()
    {
        ClientId = "admin-cli",
        ClientSecret = "your-client-secret"  // null for public clients
    };
});

Configuration from appsettings.json

builder.Services.AddKeycloakClient(builder.Configuration);
{
  "Keycloak": {
    "KeycloakBaseUrl": "https://keycloak.example.com",
    "Realm": "my-realm",
    "Credentials": {
      "ClientId": "admin-cli",
      "ClientSecret": "your-client-secret"
    }
  }
}

Custom section name:

builder.Services.AddKeycloakClient(builder.Configuration, sectionName: "Auth:Keycloak");

Options are validated at startup via IValidateOptions<KeycloakClientOptions>. If required fields are missing, the app throws OptionsValidationException at app.Run().

All Options

Option Type Required Description
KeycloakBaseUrl string Yes Base URL of your Keycloak server
Realm string Yes Keycloak realm name
Credentials.ClientId string Yes Client ID for API authentication
Credentials.ClientSecret string? No Client secret. null for public clients.

Users

keycloak.Users.*

Create, Update, Delete

// Create
string userId = await keycloak.Users.CreateAsync(new CreateUserRequest
{
    Username = "john.doe",
    Email = "john@example.com",
    FirstName = "John",
    LastName = "Doe",
    Enabled = true,
    RequiredActions = ["UPDATE_PASSWORD"],
    Credentials = [new UserCredential { Value = "temp-password", Temporary = true }],
    Groups = ["developers"],
    Attributes = new() { ["department"] = ["engineering"] }
}, ct);

// Update (all fields optional)
await keycloak.Users.UpdateAsync(userId, new UpdateUserRequest
{
    FirstName = "Jonathan",
    Email = "new@example.com"
}, ct);

// Delete
await keycloak.Users.DeleteAsync(userId, ct);

Query

var user = await keycloak.Users.GetByIdAsync(userId, ct);
var user = await keycloak.Users.GetByUsernameAsync("john.doe", ct);
var user = await keycloak.Users.GetByEmailAsync("john@example.com", ct);
var user = await keycloak.Users.GetByAttributeAsync("employeeId", "12345", ct);
var users = await keycloak.Users.GetByAttributeManyAsync("department", "engineering", ct);
int count = await keycloak.Users.GetCountAsync(ct);

var users = await keycloak.Users.SearchAsync(new UserSearchRequest
{
    Search = "john",
    EmailVerified = true,
    Enabled = true,
    First = 0,
    Max = 20,
    Exact = false
}, ct);

var all = await keycloak.Users.GetAllAsync(first: 0, max: 100, ct);

Enable / Disable

await keycloak.Users.EnableAsync(userId, ct);
await keycloak.Users.DisableAsync(userId, ct);

Credentials & Email Actions

await keycloak.Users.ResetPasswordAsync(userId, "new-password", temporary: true, ct);
await keycloak.Users.SendPasswordResetEmailAsync(userId, ct);
await keycloak.Users.SendVerifyEmailAsync(userId, ct);
await keycloak.Users.ExecuteActionsEmailAsync(userId, ["UPDATE_PASSWORD", "VERIFY_EMAIL"], ct);
await keycloak.Users.UpdateAttributesAsync(userId,
    new Dictionary<string, List<string>> { ["department"] = ["engineering"] }, ct);

Cross-Resource Shortcuts

// Groups (also available on keycloak.Groups)
var groups = await keycloak.Users.GetGroupsAsync(userId, ct);
await keycloak.Users.AddToGroupAsync(userId, groupId, ct);
await keycloak.Users.AddToGroupByNameAsync(userId, "developers", ct);
await keycloak.Users.RemoveFromGroupAsync(userId, groupId, ct);

// Realm roles (also available on keycloak.Roles)
var roles = await keycloak.Users.GetRealmRolesAsync(userId, ct);
var effective = await keycloak.Users.GetEffectiveRealmRolesAsync(userId, ct);
var available = await keycloak.Users.GetAvailableRealmRolesAsync(userId, ct);
await keycloak.Users.AssignRealmRolesAsync(userId, [adminRole], ct);
await keycloak.Users.RemoveRealmRolesAsync(userId, [adminRole], ct);

// Client roles (also available on keycloak.Roles)
var clientRoles = await keycloak.Users.GetClientRolesAsync(userId, clientUuid, ct);
await keycloak.Users.AssignClientRolesAsync(userId, clientUuid, [editorRole], ct);

// Federated identities (also available on keycloak.FederatedIdentities)
var identities = await keycloak.Users.GetFederatedIdentitiesAsync(userId, ct);
await keycloak.Users.LinkFederatedIdentityAsync(userId, "google", identity, ct);
await keycloak.Users.UnlinkFederatedIdentityAsync(userId, "google", ct);

// Sessions (also available on keycloak.Sessions)
var sessions = await keycloak.Users.GetSessionsAsync(userId, ct);
await keycloak.Users.LogoutAsync(userId, ct);

Groups

keycloak.Groups.*

// CRUD
var groups = await keycloak.Groups.GetAllAsync(ct);
var group = await keycloak.Groups.GetByIdAsync(groupId, ct);
var group = await keycloak.Groups.GetByNameAsync("developers", ct);
int count = await keycloak.Groups.GetCountAsync(ct);

string groupId = await keycloak.Groups.CreateAsync(
    new CreateGroupRequest { Name = "developers" }, ct);
await keycloak.Groups.UpdateAsync(groupId,
    new CreateGroupRequest { Name = "engineers" }, ct);
await keycloak.Groups.DeleteAsync(groupId, ct);

// Members
var members = await keycloak.Groups.GetMembersAsync(groupId, first: 0, max: 50, ct);
var members = await keycloak.Groups.GetMembersByGroupNameAsync("developers", ct);

// Sub-groups
var children = await keycloak.Groups.GetSubGroupsAsync(parentGroupId, ct);
string childId = await keycloak.Groups.CreateSubGroupAsync(parentGroupId,
    new CreateGroupRequest { Name = "frontend" }, ct);

// Membership (cross-resource — also on keycloak.Users)
await keycloak.Groups.AddMemberAsync(userId, groupId, ct);
await keycloak.Groups.AddMemberByNameAsync(userId, "developers", ct);
await keycloak.Groups.RemoveMemberAsync(userId, groupId, ct);
await keycloak.Groups.RemoveMemberByNameAsync(userId, "developers", ct);

Roles

keycloak.Roles.*

// Realm roles
var roles = await keycloak.Roles.GetAllRealmAsync(ct);
var role = await keycloak.Roles.GetRealmByNameAsync("admin", ct);

// Client roles
var roles = await keycloak.Roles.GetAllClientAsync(clientUuid, ct);
var role = await keycloak.Roles.GetClientByNameAsync(clientUuid, "editor", ct);

// User realm roles (cross-resource — also on keycloak.Users)
var assigned = await keycloak.Roles.GetUserRealmAsync(userId, ct);
var effective = await keycloak.Roles.GetUserEffectiveRealmAsync(userId, ct);
var available = await keycloak.Roles.GetUserAvailableRealmAsync(userId, ct);
await keycloak.Roles.AssignRealmToUserAsync(userId, [adminRole], ct);
await keycloak.Roles.RemoveRealmFromUserAsync(userId, [adminRole], ct);

// User client roles (cross-resource — also on keycloak.Users)
var assigned = await keycloak.Roles.GetUserClientAsync(userId, clientUuid, ct);
var effective = await keycloak.Roles.GetUserEffectiveClientAsync(userId, clientUuid, ct);
var available = await keycloak.Roles.GetUserAvailableClientAsync(userId, clientUuid, ct);
await keycloak.Roles.AssignClientToUserAsync(userId, clientUuid, [editorRole], ct);
await keycloak.Roles.RemoveClientFromUserAsync(userId, clientUuid, [editorRole], ct);

Clients

keycloak.Clients.*

var clients = await keycloak.Clients.GetAllAsync(ct);
var client = await keycloak.Clients.GetByClientIdAsync("my-app", ct);

// Client secret
var secret = await keycloak.Clients.GetSecretAsync(clientUuid, ct);
var newSecret = await keycloak.Clients.RegenerateSecretAsync(clientUuid, ct);

// Service account user
var serviceUser = await keycloak.Clients.GetServiceAccountUserAsync(clientUuid, ct);

Sessions

keycloak.Sessions.*

var sessions = await keycloak.Sessions.GetUserSessionsAsync(userId, ct);
await keycloak.Sessions.LogoutUserAsync(userId, ct);   // all sessions
await keycloak.Sessions.DeleteAsync(sessionId, ct);     // specific session

Authentication

keycloak.Authentication.*

// Login (password grant)
var token = await keycloak.Authentication.LoginAsync(new LoginRequest
{
    Username = "john.doe",
    Password = "password"
}, ct);

// Refresh token
var newToken = await keycloak.Authentication.RefreshTokenAsync(token.RefreshToken!, ct);

// Logout
await keycloak.Authentication.LogoutAsync(token.RefreshToken!, ct);

// UserInfo (requires an access token)
var userInfo = await keycloak.Authentication.GetUserInfoAsync(token.AccessToken, ct);

// Token introspection
var introspection = await keycloak.Authentication.IntrospectTokenAsync(token.AccessToken, ct);
if (!introspection.Active)
{
    // Token is expired or revoked
}

The KeycloakToken response includes AccessToken, RefreshToken, ExpiresIn, RefreshExpiresIn, TokenType, and Scope.

Security note: The password grant (grant_type=password) is deprecated by OAuth 2.1. For browser-based applications, use Authorization Code + PKCE instead. The password grant is appropriate for trusted backend services and migration scenarios.


Impersonation

keycloak.Impersonation.*

var result = await keycloak.Impersonation.ImpersonateUserAsync(userId, ct);
// result.SameRealm — whether impersonation is within the same realm
// result.Redirect  — redirect URL for the impersonated session

Note: Requires the impersonation realm role and the impersonation feature enabled on the realm.


Federated Identities

keycloak.FederatedIdentities.*

var identities = await keycloak.FederatedIdentities.GetForUserAsync(userId, ct);

await keycloak.FederatedIdentities.LinkAsync(userId, "google", new FederatedIdentity
{
    IdentityProvider = "google",
    UserId = "google-user-id",
    UserName = "john@gmail.com"
}, ct);

await keycloak.FederatedIdentities.UnlinkAsync(userId, "google", ct);

Token Management

The library manages admin API tokens automatically:

  • Admin token — acquired via client credentials flow, cached in MemoryCache, refreshed before expiry (30s buffer, clamped to minimum 30s).
  • Stampede-safe — a SemaphoreSlim with double-checked locking ensures that concurrent requests on a cold cache don't trigger multiple token fetches.
  • Thread-safe — all operations are safe for concurrent use across scoped DI lifetimes.

You never need to manage admin tokens manually. User tokens (from Authentication.LoginAsync) are returned directly — cache them in your application if needed.


Common Gotchas

Client must have service-accounts-enabled

The admin token handler uses the client credentials flow. Your Keycloak client must have:

  • Client authentication: ON
  • Service accounts roles: ON (enables service-accounts-enabled)
  • The service account must have the realm-managementmanage-users role (and other manage-* roles for groups, roles, clients, etc.)

Without this, the token fetch will fail with 401.

ClientSecret is optional

Public clients (e.g. admin-cli in development) don't have a secret. Set ClientSecret = null (the default). However, production admin API usage should always use a confidential client with a secret.

Trailing slash in KeycloakBaseUrl

The library normalizes trailing slashes automatically. Both https://keycloak.example.com and https://keycloak.example.com/ work correctly.

HTTP Resilience

Both the admin and token HttpClient instances have Microsoft.Extensions.Http.Resilience standard handlers applied automatically. This provides retries with exponential backoff, circuit breaker, and request timeout out of the box.


How It Works

┌──────────────────────────────────────────────────────┐
│  AddKeycloakClient(options)                          │
└─────────────────────┬────────────────────────────────┘
                      │
       ┌──────────────┼──────────────┐
       ▼              ▼              ▼
┌────────────┐ ┌────────────┐ ┌────────────────────┐
│ Admin      │ │ Token      │ │ Sub-Clients        │
│ HttpClient │ │ HttpClient │ │                    │
├────────────┤ ├────────────┤ ├────────────────────┤
│ BaseUrl +  │ │ BaseUrl    │ │ Users, Groups,     │
│ /admin/    │ │ (token     │ │ Roles, Clients,    │
│ realms/    │ │ endpoint)  │ │ Sessions, Auth,    │
│ {realm}/   │ │            │ │ Impersonation,     │
│            │ │            │ │ FederatedIdentities│
│ + Admin    │ │ + Standard │ │                    │
│   Token    │ │   Resilience│ │ → IKeycloakClient │
│   Handler  │ │   Handler  │ │   (facade)        │
│ + Standard │ │            │ │                    │
│   Resilience│ │            │ │                    │
└────────────┘ └────────────┘ └────────────────────┘
  1. Options validationIValidateOptions<KeycloakClientOptions> validates required fields at startup. Fails fast with OptionsValidationException.
  2. HttpClient registration — Two named HttpClient instances: admin (with auto-token DelegatingHandler) and token (for user auth flows). Both have standard resilience handlers.
  3. Token handlerAttachAdminAccessTokenHandler acquires a client-credentials token on first use, caches it in MemoryCache, and uses SemaphoreSlim + double-check to prevent stampede.
  4. FacadeIKeycloakClient is the single injection point. Each sub-client (Users, Groups, etc.) owns its own slice of the Keycloak Admin REST API.

Requirements

  • .NET 10 or later
  • A Keycloak server (tested with Keycloak 26+)

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
0.0.3 90 5/18/2026
0.0.2 112 4/23/2026
0.0.1 97 5/12/2026