DEV Community

ThatcherCole8235
ThatcherCole8235

Posted on

Node.js Roles: 3 Reasons Your Database Ages Better Than Provider Metadata

TL;DR: Keep authoritative B2B organization membership and roles in your application database. Put only a small, short-lived authorization snapshot in identity-provider metadata or tokens, and treat it as a cache. For a media service scoring login risk from device fingerprints, this split matters most during account recovery: recovery proves control of an account, then the application reloads current organization access instead of reviving whatever roles happened to be present before the lockout.

The simple design is tempting. Add org_id and role to provider metadata, copy them into a token, and avoid a database read. It works until one person belongs to several publishers, an editor changes desks, or an account recovery happens while an administrator is removing access. The chosen design adds one lookup at the authorization boundary, but it gives membership changes one place to take effect. I would spend that read deliberately; I would not spend correctness to remove it.

This is an authorization-lifetime problem, not a vendor-selection problem. Authentication establishes who is acting. Your media application still has to decide what that actor may do for a particular newsroom, title, or advertising account right now.

Should you store roles in auth provider metadata or your database?

A device fingerprint can help estimate whether a login is familiar, but it is probabilistic input. Browsers change, storage is cleared, devices are shared, and attributes can be imitated. OWASP recommends risk-based authentication signals such as device information as context for reauthentication; it does not make a device identifier proof of identity. Keep that distinction visible in the data model. Consider an editor who belongs to two publisher organizations. A login from a new device raises the risk score, so the service sends the user through an account recovery path. During that interval, one publisher removes the editor's ability to schedule stories. If recovery merely issues credentials from old provider metadata, the recovered session may restore obsolete authority. The dangerous event is not the new device. It is authorization state crossing a recovery boundary without being re-evaluated. Recovery should therefore have a narrow result: the user has satisfied the configured recovery checks. After that result, fetch active memberships and compute access under current policy. OWASP also advises invalidating sessions and rotating tokens after reauthentication for sensitive events. A recovered browser should not inherit a pre-recovery session as if nothing changed.

This rule handles the opposite failure too. If an administrator grants incident-publishing access while the user is recovering an account, a fresh database read exposes the grant without waiting for metadata synchronization. Both revocation and grant use the same path.

Draw three ownership boundaries

The first boundary is identity. Stable subject identifiers, verified contact state, authentication methods, and authentication events belong with the authentication system. They answer who authenticated and how. Avoid using mutable email addresses as application membership keys; OpenID Connect defines sub as a locally unique and never reassigned identifier within the issuer. Store the issuer and subject pair when identities can come from more than one issuer.

The second boundary is organization authorization. Membership status, role assignments, newsroom scope, temporary grants, and who changed them belong in the application database because the application owns their meaning and lifecycle. A publisher_admin role is not universal identity data. It is a relationship between a subject and one organization, interpreted by this product.

The third boundary is delivery. Tokens or provider metadata may carry a compact snapshot that avoids repeated work inside a short request path. Include enough context to reject misuse: subject, organization, issued and expiry times, and an authorization version or equivalent change marker. Do not place the full permission graph there. JWT defines exp as the time on or after which a token must not be accepted, but expiration alone does not make a role current before that time.

State System of record Safe delivery form Recovery behavior
Issuer and subject Authentication system Token claims Reconfirm identity
Device-risk evidence Risk service or application Server-side decision input Recalculate; never grant a role
Organization membership Application database Short-lived snapshot Reload active row
Scoped role assignment Application database Derived permissions Re-evaluate current policy
Authorization version Application database Token or session marker Compare before access

Three boundaries. One authority.

Provider metadata can still be useful for coarse routing or bootstrap data, especially before the application has created its local subject row. The trade-off is explicit: every duplicated authorization field needs a synchronization rule, an owner, and a maximum stale interval. If nobody can state those three things, remove the duplicate.

A focused Node.js decision path

The following TypeScript keeps the interfaces generic. It assumes authentication has already validated the credential's signature, issuer, audience, and time constraints. The function does not trust a role claim to authorize a recovered account; it loads current membership and binds the risk outcome to the recovery path.

type AuthContext = {
  issuer: string;
  subject: string;
  recovered: boolean;
  authzVersion?: number;
};

type LoginEvidence = {
  deviceRisk: number; // Normalized by the application's risk service.
};

type Membership = {
  orgId: string;
  status: "active" | "suspended";
  role: "viewer" | "editor" | "publisher_admin";
  authzVersion: number;
};

interface MembershipStore {
  findActive(
    issuer: string,
    subject: string,
    orgId: string,
  ): Promise<Membership | null>;
}

type AccessDecision =
  | { allow: true; role: Membership["role"]; authzVersion: number }
  | { allow: false; reason: "membership_missing" | "step_up_required" };

async function authorizePublishing(
  auth: AuthContext,
  evidence: LoginEvidence,
  orgId: string,
  memberships: MembershipStore,
): Promise<AccessDecision> {
  const membership = await memberships.findActive(
    auth.issuer,
    auth.subject,
    orgId,
  );

  if (!membership || membership.status !== "active") {
    return { allow: false, reason: "membership_missing" };
  }

  const recoveryNeedsStepUp = auth.recovered && evidence.deviceRisk >= 0.7;
  if (recoveryNeedsStepUp) {
    return { allow: false, reason: "step_up_required" };
  }

  // The database role wins even when a token snapshot is stale.
  return {
    allow: true,
    role: membership.role,
    authzVersion: membership.authzVersion,
  };
}
Enter fullscreen mode Exit fullscreen mode

The 0.7 threshold is an example policy value, not a universal security constant. Calibrate it from your own labeled outcomes, and separate the score from the action. A score can trigger step-up authentication or review; it should not manufacture organization membership. In production, return the version in a refreshed server session only after the database decision succeeds.

There is also a cost choice here. Reading membership on every asset request could be wasteful, while trusting a long-lived snapshot on every privileged publishing action is too loose. Cache by organization and subject for ordinary reads, then bypass or invalidate that cache for recovery, role changes, suspension, billing ownership changes, and high-impact editorial actions. The exact cache lifetime is an operational decision. The authority is not.

Test the transitions, not just the happy path

Most authorization tests create a user, assign one role, and check one endpoint. That misses the aging behavior in the original design question. Build tests around changes over time.

Use a compact transition matrix: active editor to suspended member; viewer to editor; editor removed from organization A while remaining in organization B; recovery begun before a role change and completed after it; familiar device becoming unknown after cookie loss; and two concurrent sessions carrying different authorization versions. For each transition, assert both the permitted action and the denied action. A denial deserves the same test precision as an allow.

Log decision inputs without storing a raw device fingerprint in general application logs. Useful fields include a pseudonymous subject key, organization ID, policy version, authorization version, decision, reason, risk band, and whether recovery occurred. Restrict access and retention because authentication telemetry can be sensitive. OWASP's logging guidance warns against recording session identifiers, access tokens, authentication passwords, and sensitive personal data directly.

Measure four things before copying this architecture: membership-read latency at the decision point, cache staleness after a role change, denied actions after recovery, and the rate at which step-up checks later prove legitimate. The first two expose operational cost. The latter two show whether the risk rule is protecting accounts or merely frustrating editors. Do not report a single blended success rate; recovery and normal login have different consequences.

Deployment needs a failure policy as well. If the membership store is unavailable, fail closed for publishing, organization administration, and recovery completion. A read-only public article path can have a separate availability policy because it does not confer account authority. This asymmetry is intentional. One blanket fallback would hide the actual risk.

What ages well

Database-owned organization roles age better because they preserve the domain relationship that authorization depends on. Authentication metadata ages well when it stays about identity. Short-lived snapshots age well when everyone treats them as delivery artifacts with an explicit invalidation story.

The durable rule is plain: recovery restores access to an identity, not yesterday's privileges. Recalculate device risk, rotate the recovered session, reload active memberships, and evaluate the requested action under current policy. That creates a small amount of work at a boundary where stale state is expensive.

Start with the transition tests. If a role can change while a credential remains valid, you already know which copy must win.

Further reading

Top comments (0)