Join our FREE personalized newsletter for news, trends, and insights that matter to everyone in America

Newsletter
New

The Keycloak Admin Api From Typescript, Without The Surprises

Card image cap

If you automate Keycloak from Node.js (creating users during onboarding, provisioning clients per tenant, syncing groups from HR), you eventually hit the same set of friction points. I wrote keycloak-api-ts to remove the ones that bothered me most, and version 2.0 is out: it covers 100% of the Keycloak 26.8.1 Admin API (415 operations) and is tested against real 24, 25 and 26 servers on every pull request.

This post covers the problem, the solution and the trade-offs.

The problem

The official client, @keycloak/keycloak-admin-client, is maintained by the Keycloak team and is the natural first choice. In everyday backend code, though (looking at 26.8.1), it comes with a few tolls:

1. It's ESM-only. The package is "type": "module" with no CommonJS build. On Node 20.19+ and 22.12+ require() of ESM works, but Jest/ts-jest in CommonJS mode, older Node versions and some bundlers still stumble, and you end up with transformIgnorePatterns, dynamic await import() or a project migration.

2. HTTP errors are thrown. A missing user (404) or a duplicate username (409) isn't exceptional; it's business logic. Still, every non-2xx response throws a NetworkError (only some findOne calls return null on 404), so you need a try/catch and a look at error.response.status.

// official client 
try { 
  await kc.users.create({ username: 'jdoe' }); 
} catch (e) { 
  // 409? 403? network? you have to inspect the error to know 
} 

3. The default realm is global mutable state. kc.setConfig({ realmName: 'tenant-a' }) changes the realm for every following call. You can override it per call by passing realm in the payload, but that's opt-in: forget it once, and in concurrent multi-tenant code the call lands in whatever realm another request configured.

4. Tokens renew only with a refresh token. After auth(), the client renews on its own when it has a refresh token. With client_credentials, the typical service flow, Keycloak doesn't issue a refresh token by default (the client's "Use refresh tokens for client credentials grant" option is off), so once the access token expires calls fail with "Cannot refresh token". You either call auth() again or register your own TokenProvider. There's no retry on 401, no backoff retries and no rate limiting.

5. Coverage is partial. By my count on 26.8.1, the official client covers about 340 of the 415 operations in the OpenAPI spec (≈82%). Missing are, for example, role composites, groups and permissions by role name, authorization-services searches, client types, parts of organizations and workflows, localization, and the legacy client templates. For those, you're back to hand-rolled fetch calls.

The solution

npm install keycloak-api-ts 
import KeycloakAPI from 'keycloak-api-ts'; // or require('keycloak-api-ts') 
 
const api = new KeycloakAPI({ 
  baseUrl: 'https://sso.example.com', 
  credentials: { 
    grantType: 'client_credentials', 
    clientId: 'admin-service', 
    clientSecret: process.env.KEYCLOAK_CLIENT_SECRET, 
  }, 
}); 
 
const created = await api.users.create('acme', { username: 'jdoe', enabled: true }); 
 
if (created.success) { 
  console.log('new user id:', created.data); // taken from the Location header 
} else if (created.status === 409) { 
  console.log('user already exists'); 
} else { 
  console.error(created.status, created.error); 
} 

What changes:

  • Errors as values. Every call resolves to { success, status, data | error }, a discriminated union: after if (res.success), TypeScript knows data is there. The promise only rejects on network failures or when a token can't be obtained (KeycloakAuthError).
  • Realm per call. api.users.list('tenant-a') and api.users.list('tenant-b') can run in parallel on the same instance without stepping on each other.
  • Automatic tokens. Requested lazily, cached, and renewed before expires_in (using the refresh token when there is one). A 401 invalidates the token and retries the request once.
  • Resilience on the instance. Exponential-backoff retries for network errors, 408, 429 and 5xx on idempotent requests, plus a client-side rate limit (10 req/s by default). All of it lives on the library's own axios instance, so the global axios of your app is never touched.
  • Native CommonJS and ESM. The package ships both builds, so require and import work on any Node 20+, Jest in CommonJS mode included, without relying on require(esm).
  • Types generated from the official OpenAPI spec. User, Client, Realm, Group and friends come straight from the Keycloak 26.8.1 spec, and the raw schemas are exported as KeycloakSchemas.
  • create returns the id. Keycloak answers 201 with the id in the Location header; the library hands it to you in data.

Things you only learn against a real server

The OpenAPI spec doesn't tell the whole story. A few things only surfaced when the integration tests (Testcontainers running Keycloak in Docker) hit a real server:

  • Authorization policies must be created on /policy/{type}; the generic endpoint in the spec can't parse provider-specific fields.
  • Localization texts, credential labels and workflows use text or YAML bodies, not JSON.
  • Keystore downloads come back as a Buffer, and 24/25 require a keyPassword.
  • Certificate uploads must be multipart.
  • Array filters are sent as repeated query parameters (type=LOGIN&type=LOGOUT).

Each of these became a test, and CI runs the 24.0.5 / 25.0.6 / 26.8.1 matrix on every pull request.

When to stick with the official client

If your project is already ESM, always runs the latest Keycloak and you're happy with exceptions, the official client is a perfectly good choice backed by the Keycloak team. It also ships things keycloak-api-ts doesn't have yet: Shared Signals (SSF) and verifiable-credentials endpoints, which aren't even in the OpenAPI spec, and experimental access to the Admin API v2 for clients. keycloak-api-ts makes more sense when you have a CommonJS backend, multi-tenant code, servers on different versions (24 to 26), or you need endpoints the official client doesn't cover.

Links

Issues, ideas and PRs are welcome. Next on the roadmap are the Admin API v2, once it's stable, and the SSF and verifiable-credentials endpoints. If you use Keycloak from Node, tell me in the comments what your biggest pain with the Admin API is.