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

@mi9-identity/auth-middleware-express

v1.0.0

Published

Express adapter for the Mi9 JWT verifier — drop-in middleware that verifies Mi9-issued JWTs and attaches AuthContext to req.auth.

Readme

@mi9-identity/auth-middleware-express

Express middleware that verifies Mi9-issued JWTs, attaches AuthContext to req.auth, and ships scope/retailer guard factories. Layered on @mi9-identity/jwt-verifier.

Apache-2.0-licensed and free to use; access to the Mi9 Identity Platform whose tokens it verifies is governed by your commercial agreement with Mi9 Retail.

Which package do I need?

| Package | Use it when | | --- | --- | | @mi9-identity/auth-middleware-express (this one) | Your service receives Mi9-issued tokens and is an Express 5 app. | | @mi9-identity/jwt-verifier | The same, but not Express — the framework-agnostic verifier this package wraps. Installed alongside this one. | | @mi9-identity/token-client | Your service calls a Mi9 API and needs a token. |

A service that both calls Mi9 APIs and serves Mi9-authenticated requests installs the token client too.

Install

npm install @mi9-identity/[email protected] @mi9-identity/[email protected]

express is a peer dependency. Express 5 is required (peer >=5.0.0); Express 4 is no longer supported. Packages are ESM-only and require Node 24+.

Versions are semver, and the three @mi9-identity/* packages (auth-middleware-express, jwt-verifier, token-client) are released in lockstep at the same version — pin an exact version, not a ^ range. Breaking wire changes ship as new API path versions, not as new package majors.

Configuration

Verifying needs no credential: the signing keys are public and served from the issuer's JWKS URL. You need two values, both given to you by your Mi9 contact when your service is registered — the issuer URL, and the audience identifier assigned to your service.

Configure one issuer URL and one audience for your own service:

MI9_IDENTITY_ISSUER     # https://identity.mi9retail.com/api/v1  — matches the JWT `iss` claim
MI9_IDENTITY_AUDIENCE   # this service's own audience identifier (a full URL)

The issuer URL is the versioned one, and the JWKS URI is derived from it with mi9IdentityJwksEndpoint(issuer) from @mi9-identity/jwt-verifier. Audience values are full URLs (https://merchant.mi9retail.com/api), never a bare service name.

Usage

import express, { type Request } from 'express';
import { createVerifier } from '@mi9-identity/jwt-verifier';
import { mi9Auth, requireRetailer } from '@mi9-identity/auth-middleware-express';

const verifier = createVerifier({
    jwksUri: process.env.MI9_IDENTITY_JWKS_URI,
    issuers: ['https://identity.mi9retail.com/api/v1'],
    audience: 'https://merchant.mi9retail.com/api',
});

const resolveRetailerCode = (req: Request): string => {
    const code = req.params.retailerCode;
    if (typeof code !== 'string' || code.length === 0) {
        throw new Error('retailerCode path parameter is required');
    }
    return code;
};

const app = express();
app.use(mi9Auth({ verifier }));

app.get(
    '/retailer/:retailerCode/orders',
    requireRetailer(resolveRetailerCode),
    (req, res) => res.json({ retailer: req.auth?.retailerId }),
);

The scope guards are not usable today. requireScope and requireAnyScope are exported and work, but Mi9 credentials are provisioned with an empty authorized-scope list, so issued tokens carry an empty scope claim and every scope guard rejects with 403 insufficient_scope. Authorize on the retailer, the product or the audience instead. Use the scope guards only once your Mi9 administrator confirms that your credentials carry authorized scopes.

Mi9AuthOptions reference

| Option | Required | Default | Purpose | | --- | --- | --- | --- | | verifier | yes | — | A Verifier from createVerifier(...). | | headerName | no | 'authorization' | Inbound header to read the Bearer token from (case-insensitive). | | requestIdHeader | no | 'X-Request-ID' | Inbound correlation-id header; its value flows to AuthEvent.request_id. | | onAuthEvent | no | no-op | Sink for the structured AuthEvent (wire your logger / analytics here). | | onError | no | RFC 6750 §3 mapping | Override the error envelope (status + headers + body) — see defaultErrorMapping. |

req.auth — AuthContext

On a successful verify, mi9Auth attaches the verified token payload to req.auth. The type is AuthContext from @mi9-identity/jwt-verifier:

import type { AuthContext } from '@mi9-identity/jwt-verifier';

| Field | Type | Notes | | --- | --- | --- | | iss | string | Issuer (versioned, e.g. …/api/v1). | | sub | string | Is the clientId (UUIDv7) — no separate clientId claim. | | aud | string \| string[] | Scalar for one audience, array for several. | | exp / iat / nbf | number | Unix seconds. | | jti | string | Token id. | | scope | string | Raw space-separated scope string. Empty today — see the note under Usage. | | scopes | readonly string[] | Pre-split, deduped view of scope. Empty today. | | retailerId | string | From the https://mi9retail.com/retailerId claim. | | product | string | Product the credential belongs to. | | storeCode | string \| null | Always present; null unless the credential is store-bound. |

Issuer-internal claims are stripped by the verifier before AuthContext is built, so nothing outside this table reaches req.auth.

createGuards — guards bound to an audit sink

The top-level requireScope / requireAnyScope / requireRetailer exports are convenience wrappers that emit nothing on rejection. Production consumers should call createGuards({ onAuthEvent }) so scope and retailer failures land in the same AuthEvent stream as token verification failures — otherwise an entire class of authorization rejection becomes invisible to your audit sink.

import { createGuards } from '@mi9-identity/auth-middleware-express';

const { requireScope, requireAnyScope, requireRetailer } = createGuards({
    onAuthEvent: (event) => log.info(event, event.event),
});

The returned object satisfies the Guards interface and is otherwise drop-in compatible with the top-level guard primitives.

Express version

This adapter requires Express 5 (peer >=5.0.0). The adapter calls next(err) itself, so mi9Auth works without additional wrappers. Express 5 also propagates rejections from async middleware automatically, so any custom middleware you write around req.auth can be plain async — no express-async-handler needed.

Default error mapping (RFC 6750 §3)

| Error | Status | WWW-Authenticate error= | Body error | | ------------------------------ | ------ | ---------------------------------------- | -------------------- | | MissingTokenError | 401 | omitted (RFC 6750 §3.1) | omitted | | InvalidTokenError family¹ | 401 | invalid_token | invalid_token | | InsufficientScopeError | 403 | insufficient_scope + scope="..."² | insufficient_scope | | RetailerMismatchError | 403 | mi9_retailer_mismatch⁴ | mi9_retailer_mismatch | | JwksUnavailableError | 503 | invalid_token + Retry-After: 30³ | invalid_token |

¹ Includes TokenExpiredError, IssuerNotAllowedError, AudienceMismatchError, AlgorithmNotAllowedError, MissingRequiredClaimError, InvalidTokenError. ² scope="..." parameter is appended automatically when requireScope / requireAnyScope is the source. Space-delimited per RFC 6749 §3.3. ³ temporarily_unavailable is registered for the authorization endpoint (RFC 6749 §4.1.2.1), not the resource server. RFC 6750 §6.2.1 registers only invalid_request, invalid_token, insufficient_scope. We use invalid_token + 503 + Retry-After to convey transience without emitting an unregistered code. ⁴ Mi9-private (non-standard). The token is fully valid — signature, exp, and aud all pass — but the retailerId claim does not match the resource. Registered codes don't fit: insufficient_scope is a scope shortage, invalid_token is a token defect. RFC 6750 §3 does not restrict the error= parameter to registered codes; the mi9_ namespace prefix marks it as application-specific.

The body shape is { error, error_description } for every row except MissingTokenError. RFC 6750 §3.1 forbids the error= parameter on a bare 401 challenge, so MissingTokenError emits a body of { error_description } only — clients narrowing on body.error must tolerate undefined. defaultErrorMapping is exported as a load-bearing primitive — consumers who want to override one error class while delegating the rest can compose:

mi9Auth({
    verifier,
    onError: (err) => err instanceof MyCustomError ? customResponse(err) : defaultErrorMapping(err),
});

To emit scope="..." in your own InsufficientScopeError instances, pass the required scope list to the constructor: new InsufficientScopeError('msg', ['orders:read', 'pos:write']).

This RFC 6750 surface, which your resource server returns to its callers, keeps the snake_case error / error_description names that RFC 6750 defines. It is not the same shape as the issuer's own error envelope, whose errorDescription field is camelCase — that one is handled by @mi9-identity/token-client.

If you override the envelope via onError, keep the WWW-Authenticate parameters: proxies and clients key off them.

AuthEvent shape is a contract

onAuthEvent receives a fixed-shape object that analytics sinks consume. Don't rename or drop fields — pass it straight to your structured logger:

mi9Auth({ verifier, onAuthEvent: (e) => log.info(e, e.event) });

Field names are snake_case by analyst convention, independent of the camelCase wire surface:

interface AuthEvent {
    event: 'auth_succeeded' | 'auth_failed';
    request_id: string;
    client_id?: string;      // present on success
    retailer_code?: string;  // present on success
    product?: string;        // present on success
    success: boolean;
    latency_ms: number;
    jti?: string;            // present on success
    failure_reason?:         // present on failure
        | 'missing_token' | 'invalid_token' | 'token_expired'
        | 'issuer_not_allowed' | 'audience_mismatch' | 'algorithm_not_allowed'
        | 'missing_required_claim' | 'insufficient_scope'
        | 'retailer_mismatch' | 'jwks_unavailable';
}

Troubleshooting

| Symptom | Likely cause | Fix | | --- | --- | --- | | Every request 401s with invalid_token right after a deploy | The token was signed by a key not yet in the cached JWKS | Self-heals within the verifier's unknown-kid cooldown (30 s by default). | | Requests 503 with Retry-After: 30 | JwksUnavailableError — the JWKS is unreachable and no fresh cache is left | Transient; confirm the service can reach ${issuer}/.well-known/jwks.json. | | 401 invalid_token with audience_mismatch in the audit event | The configured audience is a bare name, or does not match the token's aud | Set it to the full-URL audience the issuer mints for your service. | | Scope rejections never appear in your logs | The top-level guards were used instead of createGuards({ onAuthEvent }) | Build the guards with createGuards so rejections emit an AuthEvent. |


Inbound verification internals (JWKS caching, key rotation, the error taxonomy) live in @mi9-identity/jwt-verifier; outbound token acquisition lives in @mi9-identity/token-client.

License

Apache-2.0. The full text ships in the package as LICENSE.