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
<PackageReference Include="LowCodeHub.Keycloak.Client" Version="0.0.3" />
<PackageVersion Include="LowCodeHub.Keycloak.Client" Version="0.0.3" />
<PackageReference Include="LowCodeHub.Keycloak.Client" />
paket add LowCodeHub.Keycloak.Client --version 0.0.3
#r "nuget: LowCodeHub.Keycloak.Client, 0.0.3"
#:package LowCodeHub.Keycloak.Client@0.0.3
#addin nuget:?package=LowCodeHub.Keycloak.Client&version=0.0.3
#tool nuget:?package=LowCodeHub.Keycloak.Client&version=0.0.3
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.
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 pattern — keycloak.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 method — IValidateOptions + 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
- Users
- Groups
- Roles
- Clients
- Sessions
- Authentication
- Impersonation
- Federated Identities
- Token Management
- Common Gotchas
- How It Works
- Requirements
- License
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
impersonationrealm 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
SemaphoreSlimwith 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-management→manage-usersrole (and othermanage-*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│ │ │ │ │
└────────────┘ └────────────┘ └────────────────────┘
- Options validation —
IValidateOptions<KeycloakClientOptions>validates required fields at startup. Fails fast withOptionsValidationException. - HttpClient registration — Two named
HttpClientinstances: admin (with auto-tokenDelegatingHandler) and token (for user auth flows). Both have standard resilience handlers. - Token handler —
AttachAdminAccessTokenHandleracquires a client-credentials token on first use, caches it inMemoryCache, and usesSemaphoreSlim+ double-check to prevent stampede. - Facade —
IKeycloakClientis 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 | Versions 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. |
-
net10.0
- Microsoft.Extensions.Caching.Memory (>= 10.0.8)
- Microsoft.Extensions.Http.Resilience (>= 10.6.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.