npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@mpfrontend/security-bff

v1.0.0

Published

Node HTTP handler for independently deployed, product-neutral OIDC mediation. Implements Authorization Code with S256 PKCE, state and nonce, RS256 JWT/JWKS validation, opaque HttpOnly cookies, same-origin and CSRF checks, endpoint/method/role allowlists,

Readme

Security BFF

Node HTTP handler for independently deployed, product-neutral OIDC mediation. Implements Authorization Code with S256 PKCE, state and nonce, RS256 JWT/JWKS validation, opaque HttpOnly cookies, same-origin and CSRF checks, endpoint/method/role allowlists, bounded request bodies and server-held token refresh. The product supplies its API routes and identity-provider settings. No token crosses the presentation boundary, and no upstream response header crosses it except the content headers that a streamed route names (see "Streamed routes"). The browser calls Next.js; only its server calls this BFF.

An application can declare an allowlist with supportedUiLocales. A matching ui_locales value on /login, or the matching value of an optional uiLocaleCookie, is forwarded through the standard OIDC authorization request; unlisted values are ignored and an explicit query value wins over the cookie. The identity provider remains responsible for translated login, registration and recovery messages.

contextClaims is an allowlist of claim names that /context returns under claims. The claims come from the validated ID token at sign-in and from a refreshed ID token of the same subject at refresh (contextClaimSource: 'id', the default); contextClaimSource: 'access' takes them from the access token instead. Only plain values are projected: a string of at most 512 characters, a finite number, a boolean or a list of at most sixteen short strings. Objects, token-shaped strings and values beyond the 4 KiB budget of the projection are left out, and a claim name that refers to a token (*token*, at_hash, c_hash, nonce) is refused at startup. Tokens never reach /context, and no claim is written into a cookie.

preferenceCookie: {name, locales, themes} names the preference cookie that an app shares with the identity provider's pages (language and theme only, value lang=<locale>&theme=<theme>). On /login the BFF takes ui_locales from an explicit query value first, then the cookie's language when it is allowlisted, then the older whole-value uiLocaleCookie. An invalid value is ignored and never echoed; the cookie never decides anything about authority.

apiLocales: {supported, defaultLocale} is the allowlist for the Accept-Language header the BFF sends upstream. The browser's header is negotiated against it by quality, exact range and primary language; only an allowlisted token is ever sent, and the default replaces anything else, including malformed or crafted input. Without configuration only en is sent: English is the one language MP Frontend ships. A product lists the locales its backends answer in, which may exclude English, for example {supported: ['<code>'], defaultLocale: '<code>'}.

The default memory vault remains a single-process development profile; restart signs users out. The ./session-store export provides SessionVault, createMemorySessionVault, createRedisSessionVault, redisVaultSecurity and createVaultTicketStore. A development configuration (development: true) behaves as before and is still refused when NODE_ENV=production.

Client authentication

clientAuthentication configures how the BFF authenticates to the token endpoint, per BFF:

  • {method: 'private_key_jwt', key} (RFC 7523): each token and revocation request carries a client assertion signed with the host's private key, with iss and sub the client id, aud the issuer's token endpoint, a unique jti and a lifetime of CLIENT_ASSERTION_LIFETIME_SECONDS (60 seconds). key is {keyId, privateKey, algorithm?} (RS256, PS256 or ES256) or a function that returns the current one, so that the provider can rotate keys by key id. No shared secret goes over the wire.
  • {method: 'client_secret_basic', secret}: the secret, or a function that returns it, goes only into the Authorization: Basic header of provider requests (RFC 6749 section 2.3.1).
  • {method: 'none', publicClient: true}: a public client, sent with its client id only.

The key or secret is supplied by the host and only referenced: the package never stores it in a session, logs it, returns it in an error or a response, or puts it in /context. The PKCE, state and nonce checks are unchanged.

Session cookie and lifetimes

session: {sameSite, idleTimeoutSeconds, absoluteLifetimeSeconds, authorizationClaims} sets the session policy:

  • The session cookie may carry the __Host- prefix (Secure, Path=/, no Domain); the production profile adds it by default (production.hostPrefix: false opts out), and the prefix requires an https public origin. The cookie holds only the opaque session id, never a token or a claim.
  • sameSite applies to the session cookie: Strict is recommended and is the production default; the development default stays Lax. The login transaction cookie is always Lax, so that the provider's redirect back carries it. With Strict, the callback answers with a small same-origin page that moves on to /, because a browser does not send a Strict cookie on the next hop of a cross-site redirect chain.
  • idleTimeoutSeconds (default 1800, 60 to 86400) and absoluteLifetimeSeconds (default 28800, 300 to 604800, at least the idle timeout): a value outside its bounds refuses startup rather than being shortened. An idle or expired session fails closed with 401 and is removed. Activity is recorded under the session lease at most once a minute.
  • The session id is new after every sign-in (an earlier id in the request is removed), and it rotates at refresh when an authorization claim changes (realm_access, resource_access, groups, scope, tenant_id by default); the response sets the new cookie and the old id stops working. A change of subject or tenant still ends the session.

Re-authentication and step-up

A route may require a recent sign-in and, optionally, an authentication context class: authentication: {maxAge, acr} on the route, where acr lists the values that count and must come from the BFF's acrValues allowlist; maxAge alone needs no acr levels. When the session's auth_time is older than maxAge or its acr is not listed, the BFF answers 401 with a typed body, {title: 'STEP_UP_REQUIRED', stepUp: {maxAge, acrValues, login, resubmit}}, without calling the API. An upstream 401 with an RFC 9470 challenge (error="insufficient_user_authentication", max_age, acr_values) becomes the same typed answer; its body and other headers stay behind, and the answer never names the provider's origin. login is the BFF's own relative login path.

/login accepts max_age, acr_values (allowlisted values only) and return_to. The callback accepts the new session only when the new ID token's auth_time is within max_age and its acr is one of the requested values; otherwise it answers 401 STEP_UP_FAILED, creates nothing and leaves the earlier session as it was. A successful step-up replaces the weaker session: its id and tokens are removed. return_to must be a same-origin path that matches returnPaths (default /); another origin, a protocol-relative or backslash path, or an unlisted path is refused with 400 before the provider is asked.

The BFF never holds or replays a write. resubmit is idempotent for a safe request or a write that carries an Idempotency-Key, and manual otherwise: the presentation server resubmits an idempotent request with the same key after the step-up, and asks the person again for any other write.

Back-channel logout

backChannelLogout: true enables POST /backchannel-logout (OpenID Connect Back-Channel Logout 1.0). The body is the form logout_token=<JWT>. The token must be signed with the issuer's keys, carry the issuer as iss, the client id in aud, iat, exp and a jti, contain the back-channel logout event, name a sid, a sub or both, and contain no nonce; it is accepted for five minutes after iat. A valid token removes every session of its sid, or of its subject when it has no sid, and the next request of that browser gets 401. The answer is an empty 200; no session list or count is ever returned. A replayed jti or any invalid token is answered 400 and changes nothing. While the session vault is unavailable the endpoint answers 503 without recording the token, so that the provider's retry succeeds, and no session is served meanwhile.

The vault indexes each session by the provider's session id (from the ID token's sid) and by subject; the Redis index holds hashed session addresses only. A session is indexed before it is stored, at sign-in and when its id is rotated at refresh, so that a failed index leaves no session that a logout could not find. A vault without indexSession and revokeIndexed refuses the option at startup. The endpoint carries no browser session or CSRF token: route it only from the identity provider's network path, for example a separate internal listener or an ingress rule that admits the provider alone, and never expose it to browsers.

Streamed routes

A route of the allowlist may declare stream, for files of several megabytes that pass between the browser and a backend. A route without it behaves exactly as before: the body is read whole up to 64 KiB, it must be application/json, the answer is written as JSON with fixed headers, and the upstream call is bounded by 15 seconds. An in-process route (invoke) cannot declare a stream; its contract is unchanged.

{
  method: 'PUT', pattern: /^\/v1\/documents\/[a-z0-9-]+$/, origin: 'https://documents.internal',
  stream: {
    requestTypes: ['<media type>'],          // a body of any other type, or none, is refused with 415
    maxRequestBytes: <bytes>,                 // a declared Content-Length above it is refused with 413
    responseTypes: ['<media type>'],          // an answer of any other type is refused with 502
    responseHeaders: ['content-type', 'content-disposition'],
    idleTimeoutMs: <ms>, totalTimeoutMs: <ms>, sessionCheckMs: <ms>,
  },
}

Every value is the consumer's: the package ships no list of media types, no size, no timeout and no route. createBff refuses an incomplete or out-of-bounds policy with INVALID_ROUTE_STREAM: media types are explicit type/subtype values without parameters or wildcards, a GET route has no request types or size, responseHeaders comes from the closed set STREAM_RESPONSE_HEADERS (content-type, content-length, content-disposition, content-security-policy) and includes content-type, and each time is between 10 ms and one day with the idle timeout and the session check no longer than the total. A route whose responseTypes lists a type that runs script when a browser shows it inline on the app's origin (HTML, XHTML, SVG, any XML type, script) is refused with INVALID_ROUTE_STREAM:SCRIPT_CAPABLE_TYPE unless responseHeaders names both content-disposition and content-security-policy, so that the upstream can serve such an answer as an attachment or under a policy that sandboxes it.

Before any byte of the body is read, a streamed request passes every check of an ordinary route, in the same order: the session and its refresh, Origin and the CSRF token for every method other than GET, the allowlist and its roles, the Idempotency-Key rule, the correlation id and the route's sign-in requirement. Then:

  • Size known first. A request with a body must declare Content-Length: without it the answer is 411 LENGTH_REQUIRED, above maxRequestBytes it is 413 BODY_TOO_LARGE, and a media type (its parameters ignored) outside requestTypes is 415 MEDIA_TYPE_NOT_ALLOWED, each before any upstream call. Such a refusal closes the connection, so that an unread body is never read only to be discarded. The count of bytes is also checked as they stream.
  • Headers upstream. The upstream gets the fixed headers of every route (the access token, the negotiated Accept-Language, the correlation id and an allowlisted Idempotency-Key) and, in addition, the accepted Content-Type and the Content-Length; no other request header.
  • The body streams. The request body goes upstream as it arrives, through a pull stream that reads the next chunk from the client only when the upstream connection asks for one, so the client is held back by backpressure. The BFF holds a bounded window of a body, never all of it: the inbound socket and request buffers (Node's 64 KiB high-water mark each; the test measured at most 130,775 bytes), the chunk being forwarded and the outbound socket buffer (64 KiB high-water mark): at most about 256 KiB of process memory per transfer, whatever the size of the body. Kernel socket buffers come on top. The test measures the relay itself too: the bytes it pulled from the request minus the bytes the upstream request took from it stay at most 128 KiB (it measured none between chunks), and a negative control with a queue of four chunks fails that measure.
  • The answer streams. The answer reaches the client as it arrives, byte for byte, with the upstream status and only the headers the route names, plus X-Content-Type-Options: nosniff, Cache-Control: no-store and the correlation id. An answer whose media type is not in responseTypes is refused with 502 UPSTREAM_MEDIA_TYPE_REJECTED and none of its body is read; an answer without a body (204, 205, 304, or a Content-Length of 0) needs no type. An upstream Set-Cookie or any unnamed header never crosses. When the upstream answer is content-encoded, fetch decodes it and its Content-Length is not passed. An upstream that answers an upload before reading it whole gets its answer through with Connection: close, and the rest of the body is never read. An RFC 9470 challenge becomes the same typed step-up as on other routes.
  • The session stays checked. The session record is read again every sessionCheckMs and once more when the request body ends, before its last chunk goes upstream. A session that has ended (logout from another request, back-channel logout, a rotated session id or its absolute expiry) ends the transfer: the upstream call is aborted and the client gets 401 LOGIN_REQUIRED, or, once the answer has started, a cut connection, never a success; a session store that cannot be read ends it with 503. The session's idle timeout is not applied during a transfer, which counts as activity only when it starts.
  • Time and cancellation. A transfer with no byte moving for idleTimeoutMs, or lasting longer than totalTimeoutMs, aborts the upstream call and answers 504 UPSTREAM_TIMEOUT when no answer has started, or cuts the connection when one has. A client that cancels an upload or a download ends the upstream call at once.

The BFF never parses, inspects, stores or logs a streamed body, a file name or any content; content checks belong to the backend. Range requests, resumable uploads and multipart parsing are not provided. The presentation application relays a streamed route with createRelay of @mpfrontend/presentation-server/relay (see that package).

Production profile

A production configuration replaces development: true with a typed profile: production: {sessionVault, clientAuthentication, secureCookies: true}. createBff starts only when every condition holds, and otherwise refuses with PRODUCTION_PROFILE_REFUSED: followed by the names of the unmet conditions (productionRefusals(config) lists them without starting):

| Condition | Refusal name | |---|---| | The public origin is https | HTTPS_PUBLIC_ORIGIN_REQUIRED | | The issuer and provider origin are https | HTTPS_IDENTITY_PROVIDER_REQUIRED | | The session vault is durable (never the memory vault) | DURABLE_SESSION_VAULT_REQUIRED | | The vault is reached over TLS | SESSION_VAULT_TLS_REQUIRED | | The vault connection is authenticated | SESSION_VAULT_AUTHENTICATION_REQUIRED | | The vault's key ring is supplied by the host | HOST_KEY_RING_REQUIRED | | Client authentication is configured: a confidential method, or a public client declared explicitly with publicClient: true | CLIENT_AUTHENTICATION_REQUIRED | | Session cookies are Secure | SECURE_COOKIES_REQUIRED |

A vault describes itself through security (durable, tls, authenticated, hostKeyRing); a vault without it is treated as unknown and refused. A Redis vault is durable with a host key ring, uses TLS for a rediss: URL and is authenticated by password (or URL credentials). The production profile has no memory fallback: /health answers 503 and every request fails with 503 while the vault is unavailable. Errors carry codes only; no secret is logged. The negative controls remove each condition and observe the production tests fail. The profile does not by itself prove HA, durability policy, key custody or a reviewed deployment.

Redis records use AES-256-GCM with random 96-bit IVs and record-address associated data. Raw session IDs are hashed in keys; access/refresh tokens, CSRF, identity and PKCE data are encrypted. The active 32-byte key is supplied by the host, not generated/stored by this package. A key ID supports a read keyring during rotation. Retain old keys until their records expire or are rewritten; no automated key migration or key-management service is implemented. close() closes the owned Redis client.

GETDEL consumes login state once across replicas. Refresh uses a 30-second owner lease and an atomic Lua CAS on both the original record and current lease. Losing a lease or racing logout cannot recreate a revoked session. The absolute session lifetime is not renewed on refresh. A bounded provider transport/timeout or HTTP429/5xx failure denies the current request with generic503; only the unchanged verified server record is retained until its existing absolute expiry. No cached identity/API authority is served, no automatic retry occurs, and no unverified rotated token is saved. A later request may recover with the same opaque cookie, but ambiguous token rotation can still require a new login. Invalid grant/client, malformed response, invalid signature/claims and failed storage/CAS invalidate the record; generic503 is not enough to qualify as a provider outage. Logout removes state without refreshing even while the provider is unavailable. The readiness endpoint checks the store; storage failures return generic503 responses and never fall back to memory. Offline command queuing/reconnect is disabled, commands are bounded to two seconds; replace the adapter/replica after disconnection. This behavior needs an operational supervisor, not an unattended indefinitely reconnecting client.

The uncached security-bff:test-redis target creates its own Docker Redis 8.0 AOF instance, uses ephemeral test keys/identities and removes only its test container/anonymous volume. It verifies two BFFs, atomic callbacks, concurrent refresh, logout races, key rotation/AAD, expiry, a controlled AOF restart, store outage and Redis-backed presentation admission. It does not prove Redis cluster failover, disk/power-loss durability, TLS/ACL/key custody, central rate limits or full backend journeys. Production remains blocked on these deployment policies and complete realtime/release acceptance.

Sources: OIDC Core, PKCE RFC 7636, Keycloak OIDC endpoints.

The refresh failure distinction is a local policy: OAuth2 token errors, RFC6749 §5.2 defines invalid grants/client authentication; HTTP503, RFC9110 §15.6.4 describes service unavailability. Neither source guarantees recoverable refresh-token rotation after a lost response. Synthetic OIDC and isolated Redis tests exercise this policy, not production HA.

Storage references: node-redis, atomic GETDEL, Redis persistence tradeoffs.