Identity, Tenant Isolation, and Service Authority
Bind every human and workload request to one tenant and one explicit permission before it reaches ParcelFlow data.
The enterprise problem and today’s slice
Enterprise problem: ParcelFlow now crosses several services, so trusting a tenant ID from a URL, sharing one database role, or giving every process the same secret can turn an ordinary coding mistake into cross-customer disclosure or an unauthorized payment action.
Whole-course context: The incoming system already has durable orders, service contracts, an outbox, idempotent consumers, and bounded failure recovery; this day adds the authority evidence required before those mechanisms can safely serve multiple organizations.
Today’s slice: Authenticate human sessions and service workloads separately, authorize every action against one verified tenant, enforce tenant ownership in PostgreSQL, and deliver secrets without treating possession as unlimited authority.
End-of-day evidence: A positive order query, a forged-tenant denial, a wrong-audience workload-token denial, and a cross-tenant insert rejection share immutable request, policy, actor, tenant, and deployment identifiers.
Still unsolved: Centralized operational evidence, service-level objectives, production deployment, canary rollback, export, and retirement remain for the final three days.
Verify identity and derive trusted context
Unverified token fields are attacker-controlled text, so authorization must consume claims only after signature, issuer, audience, and time validation. OpenID Connect defines identity on top of OAuth 2.0, while JWT defines the signed claim format; neither standard says a tenant selector from a request body should be trusted.
import { createRemoteJWKSet, jwtVerify } from "jose";
type HumanContext = Readonly<{
subject: string;
tenantId: string;
permissions: readonly string[];
}>;
const keys = createRemoteJWKSet(new URL(process.env.OIDC_JWKS_URL!));
export async function authorizeHuman(
token: string,
requestedTenant: string,
loadMembership: (subject: string, tenantId: string) => Promise<readonly string[] | null>,
): Promise<HumanContext> {
const { payload } = await jwtVerify(token, keys, {
issuer: process.env.OIDC_ISSUER!,
audience: "parcelflow-api",
});
if (typeof payload.sub !== "string") throw new Error("missing_subject");
const permissions = await loadMembership(payload.sub, requestedTenant);
if (!permissions?.includes("orders:read")) throw new Error("tenant_forbidden");
return { subject: payload.sub, tenantId: requestedTenant, permissions };
}
The handler passes HumanContext, not the raw token, into the use case. Tests supply a context directly; production composition supplies the verifier. This is IoC applied to security: policy code depends on a narrow verified-context port rather than ambient headers.
Enforce tenant ownership in PostgreSQL
Application filtering is necessary but insufficient because a future query can omit it. PostgreSQL row-level security (RLS) applies a policy inside each service database, and tenant-aware local keys prevent a reservation from referencing another tenant's stock. The cross-service order_id remains opaque because PostgreSQL cannot and should not enforce a foreign key across independently owned service databases.
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
ALTER TABLE orders FORCE ROW LEVEL SECURITY;
CREATE POLICY orders_by_tenant ON orders
USING (tenant_id = current_setting('app.tenant_id', true))
WITH CHECK (tenant_id = current_setting('app.tenant_id', true));
ALTER TABLE inventory_reservations
ADD CONSTRAINT reservation_stock_tenant_fk
FOREIGN KEY (tenant_id, sku)
REFERENCES inventory_items (tenant_id, sku);
Bind the verified tenant for one transaction, never for the pooled session:
await sql.begin(async (tx) => {
await tx`SELECT set_config('app.tenant_id', ${context.tenantId}, true)`;
return tx`SELECT * FROM orders WHERE order_id = ${orderId}`;
});
The failure test deliberately omits the application predicate and still observes no foreign order row. Another test attempts to reserve a stock row owned by another tenant and expects Inventory's local composite foreign key to reject it; no Inventory migration references an Orders table.
Run negative probes before accepting the boundary
Security evidence must include an unaffected positive control, otherwise an outage can masquerade as a safe denial. The following test script exercises both human and workload boundaries:
bun test tests/security/tenant-boundary.test.ts
bun test tests/security/workload-audience.test.ts
bun run scripts/probe-auth.ts --case authorized-order-read
bun run scripts/probe-auth.ts --case forged-tenant-read
bun run scripts/probe-auth.ts --case wrong-audience-reserve
bun run scripts/probe-auth.ts --case cross-tenant-insert
Reject the day if a denial lacks actor, requested resource, verified tenant, enforcement point, expected and observed result, environment, timestamp, immutable ID, or healthy control. A 403 alone is not enough evidence.
Failure modes, trade-offs, and decision rule
Cached membership improves latency but delays revocation; short-lived service tokens reduce credential lifetime but increase broker dependency; RLS adds defense but cannot repair an incorrectly trusted tenant context. Prefer short caches with explicit invalidation, audience-specific credentials, transaction-scoped tenant binding, and database constraints. Never fail open on a cross-tenant read or privileged mutation.
Decision rule: authorize at every owning boundary from verified current context, grant the smallest action and lifetime, enforce tenant ownership again in storage, and accept the design only after positive and adversarial probes both produce immutable evidence.
Primary sources
Security behavior changes across standards, runtimes, and database versions, so unsupported memory can produce an invalid boundary. These primary specifications and vendor references anchor the claims that the implementation and negative probes must re-verify.
Key takeaways
Security controls become misleading when their responsibilities blur, because a valid credential can be mistaken for tenant or service permission. Keep these conclusions attached to the independent enforcement evidence.
- Authentication, human authorization, workload authorization, and database tenant enforcement are separate decisions.
- A token's audience and scope are enforced by the receiving service; possession is not ambient authority.
- Tenant ownership belongs in keys, relationships, transactions, and row policy, not only endpoint filters.
- Every denial needs an authorized positive control and immutable decision evidence.
Checklist
An authorization design is not ready when only its positive demo works, because cross-tenant and wrong-audience failures are the decisive safety evidence. Use this list to review both allow and denial paths before accepting the slice.
- [ ] Human token issuer, signature, audience, expiry, subject, membership, and permission are verified.
- [ ] Workload tokens are short-lived, audience-specific, tenant-bounded, and independently revocable.
- [ ] Tenant context is transaction-scoped and derived from verified authority.
- [ ] Composite relationships and RLS reject cross-tenant access.
- [ ] Secret values never enter logs or evidence records.
- [ ] Positive, forged-tenant, wrong-audience, and cross-tenant SQL probes are retained with immutable IDs.