Skip to content

padosoft/laravel-iam-node

Repository files navigation

@padosoft/laravel-iam-node

Thin, fail-closed TypeScript/Node client for the Laravel IAM control plane.

tests npm license docs

Ask the IAM server "is this subject allowed to do this?" and verify its tokens — from any Node service, with the exact same wire contract and guarantees as the PHP client. No policy logic lives here: every decision is the server's.

📚 Full documentation: doc.laravel-iam-node.padosoft.com — theory of fail-closed, decision-flow & token-verification diagrams, ADRs, Express/Fastify quickstarts, and the complete API reference.

Why

Laravel IAM is an Identity & Authorization control plane (PDP) for multi-application ecosystems. Your non-PHP services still need to ask it for decisions. This SDK is the JavaScript core of that story:

  • Fail-closed by construction. Any network error, timeout, 5xx, 4xx, malformed body, or unverifiable token resolves to deny — never allow. There is no fail-open switch. An unreachable PDP must never open the doors.
  • No PDP logic client-side. The verdict always comes from the server's decisions/check. The client never interprets grants or policies.
  • Drop-in parity. Same endpoint, payload, Bearer auth and response handling as the PHP HttpDecider — the server can't tell the callers apart.
  • Zero heavy deps. Native fetch (Node 18+) and jose for JWKS verification. ESM + CJS + types.

Install

npm install @padosoft/laravel-iam-node

Requires Node 18+ (native fetch).

Quick start

import { IamClient } from '@padosoft/laravel-iam-node';

const iam = new IamClient({
  baseUrl: 'https://iam.example.com/api/iam/v1', // full API base, incl. route prefix
  token: process.env.IAM_SERVICE_TOKEN,          // OAuth2 Client Credentials service token
  timeoutMs: 2000,
  cache: { ttlMs: 5000 },                        // optional, OFF by default
});

const decision = await iam.check({
  subject: { type: 'user', id: 'usr_123' },
  application: 'warehouse',
  permission: 'stock.adjust',
  resource: { type: 'warehouse', id: 'wh_milan' },
  context: { amount: 300 },
});

if (!decision.allowed) throw new Forbidden(decision);     // fail-closed
if (decision.requiresStepUp) promptStepUp(decision.requiredAal);

Prefer a single boolean? iam.can(query) returns true only when the PDP allowed and no step-up is pending:

if (!(await iam.can(query))) return res.status(403).end();

Fail-closed: read this

allowed === true alone is not permission. When requiresStepUp is true, the action is only permitted at a higher AAL — treat it as not yet allowed. Use iam.can() / isGranted() for the fail-safe interpretation, and only inspect requiresStepUp when you intend to drive a step-up challenge.

The cache (opt-in, off by default) never turns a deny into an allow: it stores the server's verdict verbatim, expires on a short TTL, never caches transport errors, and flushes the whole cache when the server reports a newer policy_version. Correctness before latency.

API

new IamClient(config)

Option Default Description
baseUrl — (required) Full API base, e.g. https://iam.example.com/api/iam/v1.
token Static service token sent as Authorization: Bearer.
clientId + privateKey (+ privateKeyKid) private_key_jwt (RFC 7523) — asymmetric, no shared secret. privateKey is an ES256 PKCS#8 PEM; the client signs a short-lived assertion per token request. Highest precedence. Register the matching public key (JWKS) in IAM.
clientId + clientSecret Self-managed client_credentials: the client mints/refreshes the token itself and auto-follows IAM's client-secret rotation (self-fetch), so a long-running service never breaks on a rotation and you never handle a secret by hand. Takes precedence over token.
oauthUrl <origin>/oauth OAuth base for the token + self-fetch endpoints.
timeoutMs 2000 Per-request timeout.
retries 0 Retries for idempotent network errors only (never on 4xx/5xx).
cache off { ttlMs, maxEntries? } short-TTL decision cache.
verify { issuer?, audience?, jwksUri? } defaults for verifyToken.
fetch global Inject a custom fetch (tests, proxies).

Auto-rotating credentials (recommended for long-lived services): pass clientId + clientSecret instead of a static token. The client obtains an access token via client_credentials and, when IAM auto-rotates the secret, self-fetches the new one from POST /oauth/client-secret (authenticating with the still-valid secret during the grace) and hot-swaps it — zero downtime. Enable the endpoint server-side with IAM_OAUTH_CLIENT_SELFFETCH=true. See Application credentials & lifecycle.

check(query): Promise<Decision>

POST {baseUrl}/decisions/check. Returns a normalised Decision (allowed, decisionId, policyVersion, requiresStepUp, requiredAal, matched[], explanation[]). Never throws.

can(query): Promise<boolean>

check() reduced to the fail-safe boolean (allowed and not step-up-pending).

listResources(subject, relation): Promise<Resource[]>

ReBAC list-resources (M16): the resources on which subject has relation. Fail-closed — returns [] on any error.

verifyToken(jwt, options?): Promise<Claims>

Verifies an access/ID token's ES256 signature and iss / aud / exp / nbf against the server JWKS (/.well-known/jwks.json). Resolves to the verified claims, or rejects with TokenVerificationError (the fail-closed signal — treat a rejection as deny). JWKS are cached and refetched on key rotation.

try {
  const claims = await iam.verifyToken(bearer, { audience: 'warehouse' });
  // trust claims.sub / claims.org / claims.scope
} catch {
  return res.status(401).end(); // fail-closed
}

validateManifest(manifest) / submitManifest(manifest, opts)

A node service that owns a permission catalog declares it in a manifest (a versioned file — it is your source of truth) and pushes it to IAM. validateManifest checks it locally against the laravel-iam.manifest.v2 rules (mirrors the server + the published schema at /.well-known/iam-manifest-schema.json); submitManifest POSTs it to the Admin API.

import { validateManifest, submitManifest } from '@padosoft/laravel-iam-node';

const manifest = JSON.parse(await readFile('iam.manifest.json', 'utf8'));

const { valid, errors } = validateManifest(manifest);
if (!valid) throw new Error(errors.join('; '));

const res = await submitManifest(manifest, {
  baseUrl: 'https://your-iam.example.com/api/iam/v1',
  token: process.env.IAM_TOKEN!, // needs iam:manifests.submit
});
// res.ok / res.status / res.data — IAM diffs it: additive changes apply, a removal is gated for approval
// and the removed role/permission is DEPRECATED (kept, disabled), never deleted.

Run it in CI on deploy for hands-off sync. See Keeping IAM in sync.

Middleware (Express / Fastify)

import { requirePermission } from '@padosoft/laravel-iam-node/middleware';

app.post(
  '/warehouses/:id/stock',
  requirePermission(iam, 'stock.adjust', {
    resource: (req) => ({ type: 'warehouse', id: req.params.id }),
    context: (req) => ({ amount: req.body.amount }),
  }),
  stockHandler,
);

The subject defaults to req.user.id / req.auth.sub. A missing subject, an unreachable PDP, or a pending step-up all respond 403 (fail-closed) and never call next().

Endpoint contract

This SDK speaks the canonical decision contract (01-architecture.md §12), mirroring the PHP client:

  • Endpoint: POST {baseUrl}/decisions/check
  • Auth: Authorization: Bearer <service token>, Accept: application/json
  • Body: { subject:{type,id}, permission, organization, application, resource, context, current_aal, explain }
  • Response: the server's Decision (a { "data": { … } } envelope is unwrapped transparently)

Ecosystem

This SDK is one client in the Laravel IAM family — a server (the PDP) plus a polyglot fleet of thin, fail-closed clients sharing one wire contract and one audit trail:

Documentation

Full docs — fail-closed theory, mermaid decision/token-verification diagrams, ADRs, Express & Fastify quickstarts, and a complete API reference — live at doc.laravel-iam-node.padosoft.com.

License

MIT © Padosoft · npm

About

Laravel IAM node client SDK — thin, fail-closed client over the IAM PDP/introspection (v1.5/M18)

Topics

Resources

Stars

Watchers

Forks

Releases

Used by

Contributors

Languages