This walks you from install to a route gated on a Policy Decision Point, with the fail-closed guarantees intact. For the full prerequisite matrix and configuration, see Installation.
@padosoft/laravel-iam-node is a thin client. It needs a running Laravel IAM server to talk to (the PDP) and a service token (OAuth2 Client Credentials) to authenticate with. The SDK itself decides nothing — it asks the server and fails closed on uncertainty.
Install the package
npm install @padosoft/laravel-iam-nodeRequires Node 18+ for native
fetch. The only runtime dependency isjose(JWKS verification). ESM, CommonJS and TypeScript types all ship in the box.Construct the client
ThebaseUrlis the full API base including the route prefix — identical to the PHP client’siam-client.http.base_url. Thetokenis sent asAuthorization: Beareron every PDP call.import { IamClient } from '@padosoft/laravel-iam-node'; export const iam = new IamClient({ baseUrl: 'https://iam.example.com/api/iam/v1', token: process.env.IAM_SERVICE_TOKEN, timeoutMs: 2000, // per-request budget; default 2000 verify: { audience: 'warehouse' }, // default audience for verifyToken (see step 5) });baseUrlmust include the/api/iam/v1prefix. The JWKS endpoint is derived from the origin, not the prefix (https://iam.example.com/.well-known/jwks.json), because keys live at the server root.Ask the PDP for a decision
check()returns a normalisedDecision. It never throws — every error path resolves to a deny.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) { // denied, or transport failure, or malformed response — all look the same here } if (decision.requiresStepUp) { // allowed only at a higher AAL — NOT yet permitted }decision.allowed === truealone is not permission. WhenrequiresStepUpistruethe action needs a higher assurance level. Preferiam.can()(next step), which folds both conditions into one safe boolean.Reduce to a fail-safe boolean and gate a handler
can()returnstrueonly when the PDP allowed and no step-up is pending:import { iam } from './iam'; app.post('/warehouses/:id/stock', async (req, res) => { const granted = await iam.can({ subject: { type: 'user', id: req.user.id }, application: 'warehouse', permission: 'stock.adjust', resource: { type: 'warehouse', id: req.params.id }, context: { amount: req.body.amount }, }); if (!granted) return res.status(403).end(); // fail-closed // …perform the stock adjustment… });Or let the middleware do it for you — a missing subject, an unreachable PDP, or a pending step-up all respond 403 and never call
next():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, );Verify an incoming token (authentication)
verifyToken()checks an access/ID token’s ES256 signature andiss/aud/exp/nbfagainst the server JWKS. It rejects on any problem — treat a rejection as deny.try { const claims = await iam.verifyToken(bearer, { audience: 'warehouse' }); // trust claims.sub / claims.org / claims.scope } catch { return res.status(401).end(); // fail-closed }Audience is mandatoryverifyTokenrejects if noaudienceis supplied (viaverify.audienceon the client oroptions.audiencehere). This is deliberate:josesilently skips theaudcheck when none is given, so a token minted for another service in the same cluster would otherwise verify. See Token verification theory.
What just happened
- You constructed an
IamClientpointed at the PDP’s full API base, with a service token. check()serialised your query into the exact wire body the server expects (current_aalsnake-case, all keys present,subject.typedefaulted touser) andPOSTed it to/decisions/checkwith a Bearer token.- The server’s
Decision(wrapped in a{ data }envelope) was unwrapped and normalised — missing or wrong-typed fields degrade safely (a missingallowedbecomesfalse). can()reduced the decision toallowed && !requiresStepUp— the only interpretation safe to gate on.verifyToken()fetched and cached the JWKS, then verified signature + claims, requiring an explicit audience.
Next steps
Subjects, decisions, AAL, the wire contract, the cache — the mental model behind the API.
Wire requirePermission into an Express app, with subject/resource/context resolvers.
Why every error path funnels to deny, and the threat model it defends against.