LowCodeHub.Keycloak.Client
0.0.11
See the version list below for details.
dotnet add package LowCodeHub.Keycloak.Client --version 0.0.11
NuGet\Install-Package LowCodeHub.Keycloak.Client -Version 0.0.11
<PackageReference Include="LowCodeHub.Keycloak.Client" Version="0.0.11" />
<PackageVersion Include="LowCodeHub.Keycloak.Client" Version="0.0.11" />
<PackageReference Include="LowCodeHub.Keycloak.Client" />
paket add LowCodeHub.Keycloak.Client --version 0.0.11
#r "nuget: LowCodeHub.Keycloak.Client, 0.0.11"
#:package LowCodeHub.Keycloak.Client@0.0.11
#addin nuget:?package=LowCodeHub.Keycloak.Client&version=0.0.11
#tool nuget:?package=LowCodeHub.Keycloak.Client&version=0.0.11
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?
The facade includes realm management through keycloak.Realms, client provisioning through keycloak.Clients, and role provisioning through keycloak.Roles.
| 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;
// Binds the "Keycloak" section (see Configuration below); validated at startup.
builder.Services.AddKeycloakClient(builder.Configuration);
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, .Realms, .Sessions, .Authentication, .Impersonation, .FederatedIdentities.
Table of Contents
- Configuration
- Realms
- 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",
"AdminTokenRealm": "master",
"Credentials": {
"ClientId": "admin-cli",
"ClientSecret": "your-client-secret"
}
}
}
Custom section name, plus code overrides applied after binding (e.g. a secret from a vault):
builder.Services.AddKeycloakClient(builder.Configuration, sectionName: "Auth:Keycloak",
configureOptions: o => o.Credentials.ClientSecret = secrets.KeycloakSecret);
Calling AddKeycloakClient more than once is safe: later calls only add their options configuration.
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 |
AdminTokenRealm |
string? |
No | Realm used to acquire admin access tokens. Defaults to Realm. |
Credentials.ClientId |
string |
Yes | Client ID for API authentication |
Credentials.ClientSecret |
string? |
No | Client secret. null for public clients. |
Errors and input validation
- Every admin call that gets a non-success status throws
KeycloakApiException(derives fromHttpRequestException, so existingcatch (HttpRequestException)blocks still work). It exposesStatusCode,Method,Path(no query string) andKeycloakError(Keycloak'serrorMessage/error_description). Raw response bodies, usernames and e-mails are never put in messages. Get…ByIdAsync/Get…ByNameAsyncreturnnullon 404.Authentication.*token-endpoint failures throwLoginExceptionwithStatusCodeandError.- Ids and names used as URL path segments (user id, group id, role name, client UUID, realm, provider, session id) are
percent-encoded and validated: empty,
.and..throwArgumentExceptionbefore any request is sent, soUsers.DeleteAsync("..")can never resolve toDELETE /admin/realms/{realm}.
Realms
keycloak.Realms.*
var realms = await keycloak.Realms.GetAllAsync(ct);
var realm = await keycloak.Realms.GetByNameAsync("my-realm", ct);
await keycloak.Realms.CreateAsync(new CreateRealmRequest
{
Realm = "my-realm",
Enabled = true,
ResetPasswordAllowed = true,
VerifyEmail = true
}, ct);
await keycloak.Realms.UpdateAsync("my-realm", new UpdateRealmRequest
{
DisplayName = "My Realm",
RegistrationAllowed = true
}, ct);
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, cancellationToken: ct);
await keycloak.Users.SendVerifyEmailAsync(userId, cancellationToken: ct);
await keycloak.Users.ExecuteActionsEmailAsync(userId, ["UPDATE_PASSWORD", "VERIFY_EMAIL"], cancellationToken: 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);
await keycloak.Roles.CreateRealmAsync(new CreateRoleRequest { Name = "admin" }, ct);
await keycloak.Roles.UpdateRealmAsync("admin", new UpdateRoleRequest { Description = "Administrators" }, ct);
await keycloak.Roles.DeleteRealmAsync("old-role", ct);
// Client roles
var roles = await keycloak.Roles.GetAllClientAsync(clientUuid, ct);
var role = await keycloak.Roles.GetClientByNameAsync(clientUuid, "editor", ct);
await keycloak.Roles.CreateClientAsync(clientUuid, new CreateRoleRequest { Name = "editor" }, ct);
await keycloak.Roles.UpdateClientAsync(clientUuid, "editor", new UpdateRoleRequest { Description = "Editors" }, ct);
await keycloak.Roles.DeleteClientAsync(clientUuid, "old-client-role", 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);
var clientUuid = await keycloak.Clients.CreateAsync(new CreateClientRequest
{
ClientId = "my-app",
PublicClient = true,
RedirectUris = ["https://app.example.com/*"]
}, ct);
await keycloak.Clients.UpdateAsync(clientUuid, new UpdateClientRequest
{
Name = "My App",
WebOrigins = ["+"]
}, ct);
await keycloak.Clients.DeleteAsync(clientUuid, 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 the client credentials flow and held in a private singleton (the host's
IMemoryCache, and anySizeLimiton it, is not used). Refreshed 30 s before expiry (half the lifetime for very short-lived tokens). - Stampede-safe — concurrent requests on a cold cache share one token fetch.
- Revocation-aware — a
401with a cached token evicts it (only if it is still the current one, so a burst of concurrent 401s triggers a single refresh) and retries the request once.
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
All Keycloak HttpClient instances use the Microsoft.Extensions.Http.Resilience standard handler (retries with backoff, circuit breaker, timeouts).
Retries are disabled for unsafe methods (POST/PUT/PATCH/DELETE): replaying a create after a timeout could create duplicates or
surface as a spurious 409 Conflict. Only GET/HEAD/OPTIONS are retried.
Observability
HTTP calls are traced/measured by System.Net.Http. Admin-token acquisition adds a span and two counters
(keycloak.client.admin_token.acquisitions tagged outcome, keycloak.client.admin_token.rejections):
builder.Services.AddOpenTelemetry()
.WithTracing(t => t.AddSource(KeycloakClientDiagnostics.ActivitySourceName))
.WithMetrics(m => m.AddMeter(KeycloakClientDiagnostics.MeterName));
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 — Named
HttpClientinstances: realm admin (/admin/realms/{realm}/), root admin (/admin/, forRealms), both with the auto-tokenDelegatingHandler, and token (for user auth flows). All have standard resilience handlers with retries limited to safe methods. - Token handler —
AttachAdminAccessTokenHandlerattaches the admin token from a singleton token provider (client credentials, stampede-safe, refreshed before expiry, evicted on 401). - 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.Http.Resilience (>= 10.11.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.