@xzawed/keycloak-sdk
v1.0.3
Published
Keycloak SDK for Node.js/TypeScript — OIDC/OAuth2 authentication + Admin REST API, part of a nine-language polyglot SDK
Maintainers
Readme
Keycloak SDK for Node.js
A TypeScript SDK for Keycloak covering both Authentication (OIDC / OAuth2) and the Admin REST API behind one consistent facade.
Part of a nine-language polyglot SDK (Java · Python · Node · Go · C# / .NET · PHP · Rust · Ruby · Kotlin) — idiomatic in each language, isomorphic across all of them. Monorepo: https://github.com/xzawed/KeyCloakSDK
1.0.3is on npm and holds thelatestdist-tag, so a bare install resolves it. A patch release: withexpectedAudienceset to something other than the client id (a resource server),exchangeCode(…, nonce)rejected a valid id_token issued by Keycloak asinvalid id_token, because the access token's expected audience was demanded of the id_token too. The id_token'saudis now checked for the client id (OIDC Core §2, §3.1.3.7), so the exchange passes under the override, while an id_token whoseaudlacks the client id is still refused even when it equals the override.validatestill applies the override to access tokens, and the two checks share one JWKS cache and refetch limit. The declared public API (.d.ts) is unchanged.
1.0.2was a security patch:exchangeCode(…, nonce)had not verified the id_token's signature in1.0.0and1.0.1— openid-client checks the nonce but not the signature of an id_token that comes straight from the token endpoint, so an id_token signed outside the realm JWKS (HS256, or a forged RS256) passed. Since then it goes through the SDK's hardened validator (signature ·signatureAlgorithms· iss · aud · exp), as in the other eight languages, and a missing id_token is refused.
1.0.1was a patch release of security and correctness fixes on top of1.0.0, the first release carrying the stability guarantee (the public API is under SemVer, and a breaking change requires a major bump). The PKCE verifier and tokens no longer leak throughconsole.log/JSON.stringify, and an SDK error no longer carries the IdP's raw token response in itscausechain (both present in1.0.0); the configured clock skew now actually reaches validation; JWKS responses are size-capped, an empty200key set no longer replaces a good cached one, and failed fetches back off. ⚠️ Config is now validated when it is built — aserverUrlthat is not an absolute http(s) URL, or a timeout that is not a finite number in(0, 2147483647]ms, throwsKeycloakConfigError.⚠️ Upgrading from
0.1.0? Two breaking changes since then, both at the type level only — the runtime behaviour is unchanged and the normal paths (kc.auth.validate(token),(await kc.admin()).users.search(...)) are untouched. (1)JwtValidatorcan no longer be built withnew— useJwtValidator.forJwksUri(...). (2) The five admin resource classes no longer expose their constructors in the emitted declarations;AdminClientassembles them, and they were never a consumer construction path. Both existed because the constructors were puttingjoseand@keycloak/keycloak-admin-clienttypes onto this package's public surface.
Requirements
- Node.js 22 or newer (
engines: { "node": ">=22" }) - ESM-only (
"type": "module") and async-only — every network method returns aPromise(createAuthorizationRequestis the one auth operation that is not; non-network calls such asKeycloakClient.createare synchronous too) - Ships
.d.tstype declarations, so consumers get full type checking - A running Keycloak server (26.6 verified) to connect to
Install
npm install @xzawed/keycloak-sdkA bare install resolves 1.0.3, and so does a ^1.0.0 range — at and above 1.0.0 a caret covers every 1.x, so it picks up later minor and patch releases but never a 2.0.0. Pin the exact version if you would rather not follow latest:
npm install @xzawed/[email protected]Quickstart
The same three-step shape as every other language in the monorepo — get a token → verify it → call the Admin API.
import { KeycloakClient } from '@xzawed/keycloak-sdk'
const client = KeycloakClient.create({
serverUrl: 'https://keycloak.example.com',
realm: 'my-realm',
clientId: 'my-client',
clientSecret: process.env['KEYCLOAK_CLIENT_SECRET'], // load from env / a secret manager
})
try {
// 1. Get a token via the client-credentials grant.
// TokenSet masks accessToken/refreshToken when logged or serialized.
const token = await client.auth.clientCredentialsToken()
// 2. Verify it with the hardened validator (algorithm pinning, exact iss, aud containment).
const validated = await client.auth.validate(token.accessToken)
console.log(`subject=${validated.subject} aud=${validated.audience.join(',')}`)
// 3. Call the Admin API. `admin` is created lazily on first access (client-credentials grant).
const admin = await client.admin()
const users = await admin.users.search(undefined, 0, 10)
console.log(`users=${users.map((u) => u.username).join(', ')}`)
} finally {
await client.close() // close protocol; both halves are no-ops today (global `fetch` holds no connections)
}validate() expects the token's aud to contain clientId by default, but a stock realm does not put the client id into a client-credentials token. Either pass expectedAudience: 'my-api' to create() to check the audience your tokens actually carry, or add an audience mapper to the client in Keycloak (Client scopes → dedicated scope → Add mapper → Audience).
KeycloakClient implements AsyncDisposable, so await using client = KeycloakClient.create(…) cleans up on scope exit.
For the browser/authorization-code flow, start with client.auth.createAuthorizationRequest(redirectUri) and exchange the callback with client.auth.exchangeCode(code, redirectUri, codeVerifier, nonce) — the nonce must be passed back for id_token validation to succeed.
Security defaults
Hardened JWT validation, not the unsafe library defaults:
- Algorithm pinning — signature algorithms are pinned by configuration (
RS256by default);alg: noneand header-supplied algorithms are rejected. - Strict claim checks — exact
issmatch,audcontainment check, mandatoryexp, and a bounded clock skew (30s by default). - DoS-safe JWKS refetch — the key set is refetched only on an unresolved key ID and never on a bad signature, and a cooldown rate-limits refetches to a minimum interval (
jwksMinRefetchSeconds, 30s by default) — so once the key set has been fetched, no volume of forged tokens makes the SDK issue more than one JWKS request per interval. A failed fetch is bounded separately: consecutive failures back off exponentially (0.2s, doubling, capped at 5s, with jitter), and inside that window the SDK fails fast without contacting the IdP. So a cold cache during an IdP outage no longer turns every validation into a request (measured: 20 attempts → 1 request, down from 20). ⚠️ The SDK never sleeps — it returns the error immediately, so retry pacing stays the caller's decision. - Secret handling —
clientSecretand tokens are masked as***intoString,JSON.stringify, andutil.inspectoutput; TLS verification is on by default.
Masking covers those three serialization paths, which is what most loggers reach for. It does not cover direct property access, so it is defence in depth rather than a guarantee about your logs.
Versioning and support
This SDK is 1.0 and follows SemVer: a breaking change to the public API requires a major bump. That promise is machine-backed — CI diffs this lane's public API against the previously published artifact on every build (api-extractor, whose report is diffed against the published tarball), and a removal or an incompatible change fails the build. ⚠️ This lane's gate has a known gap: it compares the emitted type surface as text, so adding a required field to an input interface passes it even though it breaks callers — that case is caught by review, not by machine. ⚠️ The gate compares the API surface. A change that leaves the surface identical but alters behaviour is not caught by it, so read the release notes before upgrading.
Only the newest released version of each language SDK receives security fixes; there are no long-term-support lines and older releases are not backported to.
Each of the nine languages versions independently. All nine reached 1.0.0 in the same release wave because they earned the same guarantee at the same time — they do not move in lockstep afterwards.
Documentation
- Project overview — all nine languages, what is identical and what is not
- Changelog — read this before upgrading; breaking changes are listed per language
- Getting started — install and quickstart for this language
- Compatibility — which Keycloak server range and base libraries each published version shipped against
- Deploying a Keycloak server
- Security policy
- Node example
License
Apache-2.0 — see LICENSE.
