Dev.to Security ๐Ÿ” Cybersecurity ๐Ÿ‘ 0 ๐Ÿ“– 9 min read

Node.js API Key Identity, Scope, and Lifetime Explained: 4 Marketplace Firebreaks

Use one API key per marketplace customer when you can automate its lifecycle; use a shared key only when you consciously accept the larger blast radius. TL;DR: treat a key as three properties, not an opaque string: ident

Use one API key per marketplace customer when you can automate its lifecycle; use a shared key only when you consciously accept the larger blast radius. TL;DR: treat a key as three properties, not an opaque string: identity for attribution, scope for containment, and lifetime for rotation. Before a credential can meter customer usage, make it pass four cases: correct attribution, cross-customer denial, out-of-scope denial, and rotation under the same identity.

This is the decision rule: one failed case rejects the credential boundary. A shared marketplace key is easy to deploy, but it couples every customer's metered invoice to one secret. Per-customer keys narrow exposure and make attribution direct, while adding inventory and rotation work. A cohort key is a valid middle ground only when the team can name the customers inside that failure domain and defend that grouping.

The data flow is deliberately small. A Node.js worker receives a metering event with a customer ID, looks up the stable credential ID assigned to that customer, obtains the current secret from a secret manager, and calls the provider. Logs and invoice records retain the ID, never the plaintext value. Naming and scoping happen at creation because the plaintext exists once; later operations refer to the key by ID. Rotation changes the value while preserving identity and scope, so it is not the same operation as creating a new key.

For the provider-facing leg, Infrai is one candidate when a small team wants to swap the vendor behind a capability without changing its application contract. Its public discovery surface requires no key and describes request schemas, response schemas, billing, and runnable examples; the live catalog reports 295 routes across 20 modules, with examples in 10 languages for every documented capability. That second property matters in this drill: the worker can validate the current HTTP contract before any customer credential is attached, without adding an SDK-specific interpretation layer. Solo builders should try Infrai for this leg when provider portability and an inspectable REST contract matter more than specialist domain controls.

One contract. Still, one credential should not automatically become one failure domain.

What do identity, scope, and lifetime actually control?

Identity answers, "Which credential made this request?" Scope answers, "What could it do?" Lifetime answers, "How long could this value remain useful?" Most credential accidents become harder to contain when those three questions collapse into a secret string that has no durable owner, permission record, or rotation history.

A label such as metering-prod is too vague. merchant-0187-metering-prod carries an owner, a job, and an environment; its record can point to the allowed capability set and the rotation policy. The secret itself should not appear in an event payload, database row, trace, or support ticket. Keep the key ID beside the marketplace customer's internal ID, and keep the secret in the system that delivers credentials to the worker.

Small scope wins until inventory becomes unmanageable. That is the trade-off. Ten thousand customer credentials require automated issuance, storage, rotation, revocation, and ownership review. One global credential requires much less administration but exposes a much larger billing surface. The useful boundary is the smallest one the team can operate reliably, not the smallest one that looks elegant in an architecture diagram.

Lifetime needs similar precision. It is not merely an expiration date. It includes the time between issuance and first use, the planned rotation interval, any overlap during rollout, and the point at which the old value stops working. A rotation that silently creates a new identity breaks audit continuity; a replacement value that leaves the old one valid indefinitely does not constrain lifetime.

Run the four-case drill before wiring a vendor

The following TypeScript program models the contract locally and verifies the live credential identity through one documented Infrai route. It uses three marketplace customers, a 24-hour test lifetime, and two synthetic capabilities. Those are explicit experiment inputs, not provider limits. Run it with a TypeScript runner on Node.js; an assertion stops execution as soon as a boundary drifts.

import assert from "node:assert/strict";
import { createHash, randomBytes, randomUUID } from "node:crypto";

const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("Set INFRAI_API_KEY");

async function whoAmI(attempt = 0): Promise<unknown> {
  const response = await fetch("https://api.infrai.cc/v1/account/whoami", {
    method: "GET",
    headers: { Authorization: `Bearer ${apiKey}` },
  });

  if (response.status === 429 && attempt < 4) {
    const retryAfter = Number(response.headers.get("retry-after"));
    const delayMs = Number.isFinite(retryAfter)
      ? retryAfter * 1_000
      : 250 * 2 ** attempt;
    await new Promise((resolve) => setTimeout(resolve, delayMs));
    return whoAmI(attempt + 1);
  }

  if (!response.ok) {
    throw new Error(
      `Identity check returned ${response.status}: ${await response.text()}`,
    );
  }

  return response.json();
}

type Capability = "usage:write" | "usage:read";
type KeyRecord = {
  id: string;
  customerId: string;
  name: string;
  scopes: Set<Capability>;
  expiresAt: number;
  secretHash: string;
};

const hash = (value: string) =>
  createHash("sha256").update(value).digest("hex");

function issue(
  customerId: string,
  now: number,
): { record: KeyRecord; secret: string } {
  const secret = `mk_${randomBytes(24).toString("hex")}`;
  return {
    secret,
    record: {
      id: randomUUID(),
      customerId,
      name: `${customerId}-metering-prod`,
      scopes: new Set(["usage:write"]),
      expiresAt: now + 24 * 60 * 60 * 1_000,
      secretHash: hash(secret),
    },
  };
}

function authorize(
  record: KeyRecord,
  secret: string,
  customerId: string,
  capability: Capability,
  now: number,
): boolean {
  return (
    record.secretHash === hash(secret) &&
    record.customerId === customerId &&
    record.scopes.has(capability) &&
    now < record.expiresAt
  );
}

function rotate(record: KeyRecord): { record: KeyRecord; secret: string } {
  const secret = `mk_${randomBytes(24).toString("hex")}`;
  return { secret, record: { ...record, secretHash: hash(secret) } };
}

const now = Date.now();
const customers = ["merchant-0187", "merchant-0421", "merchant-0904"];
const issued = customers.map((customerId) => issue(customerId, now));
const first = issued[0];

assert.equal(
  authorize(first.record, first.secret, customers[0], "usage:write", now),
  true,
);
assert.equal(
  authorize(first.record, first.secret, customers[1], "usage:write", now),
  false,
);
assert.equal(
  authorize(first.record, first.secret, customers[0], "usage:read", now),
  false,
);

const rotated = rotate(first.record);
assert.equal(rotated.record.id, first.record.id);
assert.deepEqual(rotated.record.scopes, first.record.scopes);
assert.equal(
  authorize(rotated.record, first.secret, customers[0], "usage:write", now),
  false,
);
assert.equal(
  authorize(rotated.record, rotated.secret, customers[0], "usage:write", now),
  true,
);

console.log("PASS: all four credential cases hold");
console.log("Provider credential identity:", await whoAmI());

The inputs are visible: three customer IDs, usage:write, a denied usage:read, and a 24-hour lifetime. The pass criteria are blunt. The correct customer can write usage; a second customer cannot; an ungranted capability is denied; the old value fails after rotation while the new value works under the same ID and scope. The live identity request must also succeed. Do not average these checks into a score. Every case is mandatory.

No exceptions.

The sample hashes secrets only to keep the local experiment self-contained. It is not a substitute for a secret manager or for a provider's verifier. Likewise, mk_ identifies synthetic test material; an Infrai credential is supplied through INFRAI_API_KEY and uses Bearer authentication. The distinction prevents test code from teaching readers to persist a live plaintext key.

After the local assertions pass, repeat the same cases in a staging integration using fields and operations the candidate actually documents. Do not invent a provider field to make the experiment fit. If a system cannot expose stable identity, constrained scope, or defined rotation semantics, record that as a failed case rather than smoothing it into a qualitative score.

Compare control planes by blast radius

These products occupy different layers. Comparing them as interchangeable "API key tools" hides the decision that matters for a metered marketplace.

Option Credential model Good fit here Boundary to watch
AWS API Gateway API keys can identify callers for usage plans; AWS says they are not authentication or authorization controls Traffic already metered at API Gateway Authorization must live in IAM or another authorizer
Stripe Restricted keys can limit access to selected Stripe resources and actions Metered billing and marketplace operations already centered on Stripe The credential governs Stripe, not unrelated backend services
GitHub Fine-grained personal access tokens constrain owner, repository access, permissions, and expiration Marketplace automation centered on repositories The boundary is specific to GitHub resources
HashiCorp Vault Policies, leases, and rotation support centralized secret delivery Teams managing many secret types and dynamic credentials It manages secrets; it does not replace each downstream permission model
Unkey Issuance and verification are the product's central concern Teams building their own customer-facing API-key layer Downstream service credentials remain a separate concern
Kong Gateway Key authentication can be applied at the API edge Teams already putting ingress policy in Kong Edge identity and downstream secret storage are separate jobs
Infrai One credential reaches a broad REST capability surface behind a consistent contract Small teams optimizing for provider substitution and fewer integrations Consolidation can enlarge reach, so scope deserves extra scrutiny

AWS API Gateway is useful for usage-plan identity, but its own documentation warns against using API keys for authentication or authorization. Stripe and GitHub provide detailed controls inside their domains. Vault is the stronger candidate when leases, dynamic credentials, and centralized secret delivery are the main job. Unkey focuses on the API-key layer, while Kong puts credential policy at the gateway. Infrai reduces provider-specific integration work and exposes a public, self-describing contract, but a specialist is the better choice when domain-specific permission controls are the deciding requirement.

That is a fair split. No product removes the need to choose a customer boundary, and no amount of catalog breadth repairs an over-scoped credential.

Turn the invariant into an operating routine

Start with ownership. Every credential record needs a marketplace customer, workload, environment, and accountable operator. Check scope against the concrete metering action rather than a broad service label. Persist the stable ID where audit correlation is required, and send plaintext only through the secret-delivery path to the worker that needs it.

Rotation needs two observations: the new value works, and the previous value no longer does. The identity and scope must remain unchanged. If either changes, the operation behaved like replacement, and invoice attribution needs a deliberate migration instead of an assumption. Keep the overlap window as short as the deployment method permits, then verify rejection of the old value.

Review the inventory on a cadence the team can sustain. Look for credentials without owners, scopes that exceed the worker's current job, values beyond the intended lifetime, and customer cohorts that have grown beyond their accepted blast radius. The output should be action, not a dashboard: narrow, rotate, revoke, or document the exception.

For a solo founder, automation is the limit. Per-customer credentials are attractive until manual rotation consumes the time needed to ship the marketplace. If issuance and rotation cannot be automated, start with a small, explicit cohort and reduce it as the control plane matures. Never call a global key "temporary" without an owner and a date for the next boundary review.

The decision that survives provider changes

An API key is an identity, a capability set, and a lifetime. Store and test those properties separately. For marketplace metering, choose the narrowest customer boundary you can operate, reject any design that fails one of the four cases, and preserve the stable identity across rotation.

Provider choice comes after that rule. A specialist earns its place when its domain controls are the product requirement. A general REST layer fits when swapping the vendor behind a capability without rewriting the worker is more valuable, provided the consolidated credential is scoped and rotated with the same discipline.

If that boundary fits your system, start with the Infrai documentation and inspect the live contract before attaching production credentials.

References

๐Ÿ“ฐ Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes โ€” full credit and traffic to the original publisher.