@debito/zeko-auth-guard
v0.4.0
Published
Tiny shared Hapi auth guard for Zeko services — authenticates + authorizes via uauth
Downloads
6,262
Readme
@debito/zeko-auth-guard
Tiny shared Hapi auth guard for Zeko API services. It authenticates the caller and
enforces app authorization by asking uauth (user_authentication_service) — the single
source of truth — over HTTP, on every request. uauth resolves the session in Redis, so a
terminated device fails on its next call.
What it does
Per request to a protected route:
- Read the
auth_tokencookie (the plugin registersserver.state('auth_token', …)so it's never forgotten). - Call uauth
GET /auth/authorize?appId=…forwarding the cookie (one call per request). - Map the result to Hapi auth: 401 (no/invalid/expired token, or
requireOrgand no org), 403 (app disabled for org or user lacks access), else allow — attaching{ userId, user: { id, name }, orgId, roles, permissions, is_support, authToken }torequest.auth.credentials.
Service-to-service calls
Internal callers send an X-Service-Token header (a signed service JWT). A valid service
token skips the per-user app gate — the downstream trusts the calling service.
Credentials then include { isService: true, service }. When the caller also forwards
the user's auth_token cookie (the proxy always does), they carry
userId, orgId and user: { id, name } as well (0.2.1), so a service acting for a
person can record who did it exactly as on a direct user request.
Usage
import { authGuard } from '@debito/zeko-auth-guard';
await server.register({
plugin: authGuard,
options: { appId: 'vm', requireOrg: true } // omit appId for identity-only (infra services)
});
// protect a route
server.route({
method: 'GET',
path: '/vendors',
options: { auth: 'auth-guard' },
handler: (request, h) => {
const { userId, orgId, authToken } = request.auth.credentials;
// ...
}
});Options
| option | default | meaning |
|--------|---------|---------|
| appId | null | app to authorize against; null = authenticate only (infra services) |
| requireOrg | false | reject tokens without an orgId |
Env vars: UAUTH_API_URL (uauth base URL) and SERVICE_JWT_SECRET (verifies
X-Service-Token).
The door out: createProxyRoutes
An app talks only to its own API. Anything it needs from another Zeko service goes through one route this package provides:
import { authGuard, createProxyRoutes } from '@debito/zeko-auth-guard';
server.route(createProxyRoutes('tasks', { crm: process.env.CRM_API_URL }));
// GET /proxy/crm/customers?x=1 ==> GET {CRM_API_URL}/customers?x=1The map is the whole list of what this app may reach; anything else answers
unknown_service. Method, query, body and the upstream's status and JSON pass through.
Every call carries an X-Service-Token signed on the fly from SERVICE_JWT_SECRET
({ type: 'service', service: '<appId>' }) plus the caller's auth_token cookie, so the
far guard trusts the service and still resolves the user's org. No token is stored
anywhere; the secret is the only thing a service holds.
The trail: createActivityClient
A service reports what it did to zeko-activity-api through one client this
package provides (0.3.1), one call per mutation, after its own write succeeded:
import { createActivityClient } from '@debito/zeko-auth-guard';
const activity = createActivityClient('hippo', process.env.ACTIVITY_API_URL);
await activity.post(request, {
action: 'account.update', // <entity>.<verb>
target: { type: 'account', id: 'account-bank', label: 'Bank', path: '/ledger/bank' },
scope: { book: bookId }, // optional, any shape
change: { before: { label: 'Bank' }, after: { label: 'Bank QA' } } // optional, any shape
});It signs X-Service-Token on the fly and forwards the caller's cookie, so the
far guard stamps who did it; the payload never names the actor. Anything but
201 is logged as [activity] <action> failed and returns null — the user's
action never fails because the trail did. With ACTIVITY_API_URL unset the
client warns once and every post is a no-op. This is the one function a
message queue replaces later.
The door to CouchDB: createCouch
The fleet has one CouchDB, named once in ~/pm2/shared.env as COUCHDB_URL,
COUCHDB_USERNAME and COUCHDB_PASSWORD. A service builds its client from
those three and nothing else (0.4.0):
import { createCouch } from '@debito/zeko-auth-guard';
const nanoInstance = createCouch();A missing variable throws at boot. The instance is nano with parseUrl: false,
so a path prefix in the URL (an nginx /db front) stays part of the server
base. No service carries a CouchDB address or password in its own pm2 JSON.
Service tokens for external callers
A system outside the fleet (Quicko, say) has no secret, so mint it a long-lived token:
SERVICE_JWT_SECRET=<secret> node scripts/mint-service-token.js quickoIt sends that as X-Service-Token. Internal services never need this.
Test
npm install && npm testIntegration tests: a throwaway Hapi server with a stubbed fetch — no network, no
running uauth. Covers 200 / 401 / 403 and the service-token bypass.
