Constructs a SecurityMiddleware instance.
Optionalsettings: SecuritySettings
Settings to use. Omit to take them from the configuration surface, which is what callers normally do.
Reader for the runtime's account of a request. Defaults to the Cloudflare one, which is the only thing here that knows a platform — and the only argument that would change to run this middleware elsewhere.
Settings are looked up on first use rather than here. An application
composes its app at module scope — const security = new SecurityMiddleware() beside the routes — which runs while imports are
still being hoisted, before configure() can. Resolving in this
constructor would make importing the app throw.
Private ReadonlymetadataReader for the facts the runtime attaches to a request.
Private ReadonlysettingsThis middleware's settings, resolved on first read.
PrivateallowedOrigins allowed to call this Worker from a browser. Empty allows any.
PrivateexcludedPaths the key check skips, for routes that authenticate themselves.
PrivatepublicRoutes registered ahead of authentication, and so reachable without a key.
PrivateurlThe URL pattern where security middlewares will be applied.
PrivateapiValidates the API key, comparing the configured header against the expected value from the environment.
The Hono context.
Passes control to the next middleware.
A 500 response when the Worker has no key configured, a 401
when the client's key does not match, and nothing when the request is
allowed through.
Registers CORS, the origin allowlist, the public routes, and the authenticated pattern on an app.
The Hono app to apply the middleware to.
PrivateclientCaptures the client's IP address onto the context as clientIp.
The Hono context.
Passes control to the next middleware.
Which header may be believed is a platform question, so this does not
answer it — CloudflareRequestMetadata
does, and documents why CF-Connecting-IP can be trusted where
X-Forwarded-For cannot. This middleware only puts the answer somewhere
handlers can reach it.
The address is put on the context rather than only logged, so a handler that needs it does not have to re-derive it and risk picking the wrong header:
const ip = ctx.get('clientIp')
It is not logged here. Every request already carries its IP in the
request.completed line logToAnalytics emits, and a
second line saying the same thing at debug was one more record to sample,
pay for, and read past — for a value the next line already holds.
PrivateisWhether a path is exempt from the API-key check.
A trailing /* exempts a subtree, anything else exempts that one path —
the same convention urlPattern and publicRoutes already use. Exact is
the default because the two are genuinely different intents: a health check
mounted at the pattern's own prefix must stay open without opening
everything beneath it.
The request path.
True when the blanket check must not run.
PrivateoriginRejects a browser request from an origin this Worker does not serve.
The Hono context.
Passes control to the next middleware.
A 403 when the origin is not allowed, nothing otherwise.
Runs ahead of everything else and, unlike CORS, actually refuses the request. CORS is enforced by the browser after the response is produced, so on its own it would let another site's page reach an endpoint and spend this Worker's upstream quota while merely being unable to read what came back.
A missing Origin is allowed through: server-to-server callers send none,
and they are the ones the API key exists for.
The security middleware stack applied to the Hono app: CORS, an origin allowlist, client-IP capture, and API-key authentication.
Remarks
Which routes it protects, which stay public, and which origins may reach it from a browser all come from the configuration layer, so
src/app.tscomposes the app without restating any of it. Override inconfig/SecurityConfig.ts.Authentication fails closed: a missing server-side key yields
500, a mismatched client key401, and neither reaches the route. Because the rejection short-circuits, anything that must observe rejected requests — request logging, telemetry — has to be registered ahead of SecurityMiddleware.apply.The blanket check is default-deny within its pattern: a route added later is protected until someone exempts it. Endpoints that cannot be addressed by a key — a third-party webhook, or a browser endpoint where an embedded key would be public anyway — are named in
excludedRoutesand authenticate themselves instead. An emptyurlPatterndisables the blanket check altogether, leaving CORS and the origin allowlist as all this registers.Not to be confused with an application's
config/SecurityConfig, which is the override that feeds this rather than a second implementation of it.Author
Bayu Dwiyan Satria
Version
1.0.0
Since
1.0.0