npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@jaltech/nestjs-auth-keycloak

v0.3.0

Published

Keycloak provider (OIDC discovery + JWKS + UMA) for @jaltech/nestjs-auth-core.

Readme

@jaltech/nestjs-auth-keycloak

npm version license types

[!WARNING] Pre-release — not production-ready. This package is under active development (pre-1.0.0) and has not yet been through a human stabilization and review pass. Its API may change at any time, without a deprecation cycle. This notice will be removed at the 1.0.0 release.

Keycloak provider for @jaltech/nestjs-auth-core. It relies on standard OpenID Connect (JWKS signature validation with jose) and does not use the deprecated keycloak-connect adapter.

Features

  • OFFLINE validation (JWKS, cached with cooldown) or ONLINE validation (/userinfo).
  • Realm roles (realm_access) and client roles (resource_access[client]).
  • Audience verification (aud contains the client, or azp equals it).
  • UMA 2.0 fine-grained authorization for @Resource / @Scopes, with a decision cache (in-memory or Redis).
  • Multi-tenancy: static realm allowlist, dynamic resolver backed by your database, or Keycloak Organizations (single realm).
  • OIDC Back-Channel Logout support.

Requirements

  • Node.js >= 20, ESM, NestJS 11
  • @jaltech/nestjs-auth-core (peer dependency)
  • Keycloak 26+ for the Organizations claim; any recent Keycloak otherwise

Installation

pnpm add @jaltech/nestjs-auth-core @jaltech/nestjs-auth-keycloak

Quick start (single realm)

import { AuthModule, TokenValidation } from '@jaltech/nestjs-auth-core';
import { KeycloakProvider } from '@jaltech/nestjs-auth-keycloak';

AuthModule.register({
  provider: KeycloakProvider.create({
    authServerUrl: 'https://auth.example.com', // without /auth since Keycloak 17
    realm: 'my-realm',
    clientId: 'my-api',
    verifyTokenAudience: true,
  }),
  tokenValidation: TokenValidation.OFFLINE,
});

Configuration — KeycloakProvider.create(config)

| Option | Default | Description | |---|---|---| | authServerUrl | — (required) | Keycloak base URL. | | realm | — (required) | Realm name. | | clientId | — (required) | Client of this API. | | secret | — | Client secret (confidential clients). | | backchannelUrl | authServerUrl | Base URL for server-to-server calls (JWKS, UMA, userinfo), e.g. http://keycloak:8080 on an internal network. The expected issuer stays authServerUrl. See Internal network. | | verifyTokenAudience | false | Recommended: true. Rejects tokens issued for another client of the same realm. Accepts aud containing the client, or azp === clientId (Keycloak often puts account in aud and the client in azp). | | requireOrganization | false | Reject tokens without a Keycloak Organizations claim. | | umaCacheTtl | 60000 | UMA decision cache TTL (ms). 0 disables the cache. | | umaCacheStore | in-memory | Store for UMA decisions (see below). | | realmPublicKey | — | Verify with a local public key instead of fetching the JWKS. | | minTimeBetweenJwksRequests | — | Minimum delay between JWKS fetches (seconds). | | bearerOnly, publicClient, confidentialPort, sslRequired | — | Deprecated, no effect; a warning is logged at boot. |

Security note. verifyTokenAudience defaults to false for backward compatibility. Leaving it off means a token issued in the same realm for another application is accepted. Turn it on.

UMA decision cache shared across instances (Redis)

UMA decisions are cached per sub and requested permissions. Behind a load balancer, share the cache so every instance sees the same decisions and an invalidation takes effect everywhere. The provider is built before dependency injection, so pass the store through registerAsync:

import { AuthModule, PolicyEnforcementMode, TokenValidation, UMA_CACHE, type IUmaCache } from '@jaltech/nestjs-auth-core';
import { KeycloakProvider } from '@jaltech/nestjs-auth-keycloak';
import { CacheModule } from '@jaltech/nestjs-cache';

@Module({
  imports: [
    CacheModule.register({
      store: { type: 'redis', url: process.env.REDIS_URL, namespace: 'app' },
      extras: [{ provide: UMA_CACHE, config: { type: 'redis', url: process.env.REDIS_URL, namespace: 'uma' } }],
    }),
    AuthModule.registerAsync({
      inject: [UMA_CACHE],
      useFactory: (umaCache: IUmaCache) => ({
        provider: KeycloakProvider.create({
          authServerUrl: process.env.KEYCLOAK_URL!,
          realm: process.env.KEYCLOAK_REALM!,
          clientId: process.env.KEYCLOAK_CLIENT_ID!,
          umaCacheTtl: 15 * 60 * 1000,
          umaCacheStore: umaCache,
        }),
        tokenValidation: TokenValidation.OFFLINE,
        policyEnforcement: PolicyEnforcementMode.PERMISSIVE,
        verbScopeDefaults: true,
        enforcementShadow: process.env.AUTHZ_ENFORCE !== 'enforce',
      }),
    }),
  ],
})
export class AppModule {}

Invalidating decisions

After a permission change, drop the cached decisions through the auth instance. The cache key format stays internal to this package; do not build patterns yourself.

import { AUTH_INSTANCE } from '@jaltech/nestjs-auth-core';
import type { KeycloakInstance } from '@jaltech/nestjs-auth-keycloak';

constructor(@Inject(AUTH_INSTANCE) private readonly auth: KeycloakInstance) {}

await this.auth.forgetUser(sub);          // the user's roles or groups changed
await this.auth.forgetResource('invoices'); // the resource's permissions or policies changed

Both return the number of decisions removed (0 when the cache is disabled). The store must support deleteByPattern, which InMemoryUmaCache and the @jaltech/nestjs-cache stores do; otherwise they throw. An empty identifier or one containing a glob character (*, ?, [, ], \) is rejected.

Only real decisions are cached: 200 (granted) and 403 (denied by policy). A transient failure (429, 5xx, network error) denies the current request (fail-closed) and is logged as a warning, but it is not cached.

Internal network (backchannel)

When the API and Keycloak run on the same private network, call Keycloak directly instead of going through the public URL and its reverse proxy (which usually rate-limits the token endpoint per client IP; all your users would then share the API's quota):

KeycloakProvider.create({
  authServerUrl: 'https://auth.example.com', // public URL: expected token issuer
  backchannelUrl: 'http://keycloak:8080',     // internal URL: JWKS, UMA, userinfo
  realm: 'acme',
  clientId: 'api',
});

On the Keycloak side, fix the public hostname so that tokens keep the public issuer whichever URL was used to reach Keycloak:

KC_HOSTNAME=https://auth.example.com

If the same application also uses an OIDC client library (e.g. openid-client for the login flow), keep its configuration on the public URL and route its requests internally with a custom fetch; leave KC_HOSTNAME_BACKCHANNEL_DYNAMIC off so that discovery keeps advertising https endpoints.

Multi-tenancy

Three modes, all fail-closed (an unknown issuer is rejected with 401). The token's unverified iss is only ever used as a lookup key into your allowlist; no URL is ever derived from the token.

1. Static realms (a few tenants)

KeycloakProvider.createMultiRealm({
  realms: [
    { realm: 'acme', clientId: 'api', issuer: 'https://auth.example.com/realms/acme' },
    { realm: 'globex', clientId: 'api', issuer: 'https://auth.example.com/realms/globex' },
  ],
  verifyTokenAudience: true,
});

2. Dynamic resolver (many realms)

KeycloakProvider.createMultiRealm({
  resolveRealm: async (issuer) => tenantsRepository.findRealmByIssuer(issuer), // null ⇒ 401
  resolverCacheTtlMs: 5 * 60 * 1000,
  resolverNegativeCacheTtlMs: 30 * 1000, // protects your database from garbage issuers
  jwksCacheMax: 100,                     // bounded LRU of JWKS sets
});

Resolver results are cached (positive and negative), unknown-issuer lookups are rate-limited, and JWKS sets live in a bounded LRU cache so memory does not grow with the number of realms. A token from realm A can never pass as realm B: the issuer selects the realm configuration, and the signature is verified against that realm's keys.

UMA and ONLINE validation are not available in multi-realm mode.

3. Keycloak Organizations (recommended at scale)

Beyond a few dozen tenants, prefer one realm with Keycloak Organizations (Keycloak 26+) over one realm per tenant. Each realm carries its own configuration, keys and caches: realm-per-tenant increases Keycloak's startup time, memory and administration effort, and every configuration change has to be repeated per realm. With Organizations, each customer is an organization inside a single realm, with its own members, email domains and federated identity providers.

The organization claim is normalized whatever the mapper shape (string, array, or object keyed by alias) and exposed as AuthIdentity.organizations, so your principalResolver can bind the user to its tenant. Use requireOrganization: true to reject tokens without an organization.

Back-channel logout

KeycloakProvider.create() and createMultiRealm() both expose a back-channel logout validator. Mount it with BackChannelLogoutModule from the core package and set the client's Backchannel logout URL in Keycloak to https://<api>/auth/backchannel-logout.

Migrating from @jaltech/nestjs-auth

| Before | After | |---|---| | import { Resource, Scopes, Public, UMA_CACHE, TokenValidation, PolicyEnforcementMode } from '@jaltech/nestjs-auth' | same names from @jaltech/nestjs-auth-core | | KeycloakModule.register({ authServerUrl, realm, clientId, secret, umaCacheTtl, … }) | AuthModule.register({ provider: KeycloakProvider.create({ authServerUrl, realm, clientId, secret, umaCacheTtl }), … }) | | policyEnforcement, tokenValidation, cookieKey, verbScopeDefaults, enforcementShadow on KeycloakModule | same options on AuthModule | | UMA cache injected automatically from UMA_CACHE | pass it explicitly with registerAsync (see above) |

Behavior is otherwise unchanged. Turning on verifyTokenAudience is recommended.

Contributing

See the repository README, CONTRIBUTING.md and SECURITY.md.

License

MIT © 2026 Jaltech — see LICENSE.