PAS7 Studio

Auth middleware in Bun.js: JWT, sessions, API keys, and multi-tenant context without chaos

A practical deep dive into auth middleware in Bun.js: how to build JWT, session, and API-key checks, where to keep tenant context, how to separate 401/403, what to cache, how to avoid token leaks, and which best practices to borrow from OWASP, Hono, and Elysia.

14 May 2026· 14 min read· Technology
Best forBackend engineersFull-stack developersTech leadsTeams building SaaS or internal APIs on Bun
Technical illustration of an auth middleware pipeline in Bun.js with a token, session, API key, and tenant context

The most dangerous auth bugs rarely look like “a user without a token got into the system”. More often, the token is valid, the session exists, the API key is real, but the middleware attached the request to the wrong tenant, skipped a scope check, mixed up 401 and 403, or logged a secret.

Bun gives you a fast HTTP layer, but auth is not a runtime trick. It is a contract: which credentials are accepted, where they are stored, how they are validated, who creates user context, where authorization starts, and how all of it is tested.

In this article, we will not build “another login tutorial”. We will break down production patterns for auth middleware in Bun APIs.

JWT, sessions, and API keys have different failure modes.
Authentication and authorization should not be mixed into one global middleware.
Tenant context must be explicitly derived from the credential and policy, not from an arbitrary header.
Tokens and cookies should live in logs as if they do not exist there at all.

This is the second chapter in the series about Bun middleware. The first one covered the general pipeline model; this one focuses on the auth layer: where it should stay thin, where it needs storage, and where it stops being middleware.

The mental model of an auth pipeline: extract credential, verify, resolve subject, resolve tenant, attach context, continue or reject.
JWT middleware: when it fits, which claims to verify, and why decode without verify is not auth. [4]
Session middleware: cookies, HTTPS, secure attributes, session store, and logout semantics. [1][5]
API keys: hashed storage, scopes, rotation, owner metadata, and machine-to-machine access.
Hono and Elysia: when to use built-in middleware/plugins, and when to write your own thin layer. [2][3][6][7]
Bad practices: tokens in localStorage/logs, global DB lookups, tenant from a header without verification, long-lived JWT without a revocation story.

Authentication answers the question: “who or what is making the request?” Authorization answers: “is this subject allowed to perform this exact action on this exact resource?” Middleware should often do the first part and prepare context for the second.

Good auth middleware creates an auth object: subjectId, subjectType, tenantId, scopes, sessionId, authMethod, requestId. But it should not know every business rule in the product. For example, canUpdateInvoice(invoiceId) is better checked in the route or use case, because that is where the resource context exists.

If you build a global middleware that “checks everything”, you get a policy engine without schemas, tests, or visibility. If you make the middleware too thin, every route starts reading headers and cookies by itself. The balance is a thin auth context plus explicit authorization close to the business action.

Auth middleware is easier to reason about as a state machine: the credential is extracted, verified, transformed into subject and tenant context, and then the request is either allowed or rejected.

Section auth-vs-authorization screenshot

Practical rule

Middleware should say who arrived and with which base context. Whether that subject can perform a specific action should be decided by the route, guard, or use case.

There is no single “correct” auth mechanism. There are trade-offs: stateless verification, controlled logout, machine-to-machine access, rotation, revocation, and UX.

MechanismWhen it fitsWhat to verifyMain risk
JWT access tokenMobile/API clients, service-to-service, distributed verificationexp, iss, aud, signature, algorithm, subject, scopesDifficult revocation model and the temptation to create long-lived tokens
Server-side sessionBrowser apps, dashboards, SaaS products that need controlled logoutSession id in a secure cookie, session store, expiry, device/risk metadataSession fixation, insecure cookies, weak store cleanup
API keyIntegrations, webhooks, internal automations, machine clientsHashed key lookup, scope, owner, environment, last used, rotationPlaintext key storage and keys without scopes or expiry policy
HybridBFF, enterprise SaaS, admin panels plus public APISession for browser, JWT/API keys for servicesDifferent auth models without unified audit and error contracts

JWT, sessions, and API keys solve different jobs: stateless access, browser logout control, and machine-to-machine access.

Section credential-map screenshot

Summary

For browser SaaS, usually start with sessions. For external APIs, use API keys or short-lived JWT. For distributed systems, use JWT plus revocation or introspection where the risk is high.

In native Bun.serve, you can build an auth layer without a framework. The important part is not mutating global state and not hiding context in module-level variables. Pass context explicitly: either through a wrapper or through your own AppRequestContext.

One simple pattern looks like this:

TS
type AuthContext = {
  subjectId: string;
  tenantId: string;
  scopes: string[];
  method: "jwt" | "session" | "api-key";
};

type AppContext = { auth: AuthContext | null; requestId: string };
type Handler = (req: Request, ctx: AppContext) => Promise<Response> | Response;

type Middleware = (next: Handler) => Handler;

const unauthorized = () =>
  Response.json({ error: "unauthorized" }, { status: 401 });

const withAuth: Middleware = (next) => async (req, ctx) => {
  const header = req.headers.get("authorization");

  if (!header?.startsWith("Bearer ")) return unauthorized();

  const token = header.slice("Bearer ".length);
  const auth = await verifyAccessToken(token);

  if (!auth) return unauthorized();

  return next(req, { ...ctx, auth });
};

Bun.serve({
  fetch: withAuth(async (_req, ctx) => {
    return Response.json({ subjectId: ctx.auth?.subjectId });
  }),
});

This example intentionally does not show the implementation of verifyAccessToken, because it depends on your JWT/session/API-key design. The important shape is this: middleware does not read business resources, does not perform permissions on invoices/projects/orders, and does not create side effects. It creates auth context or returns 401.

What matters here

Context must be request-scoped and explicit. If auth state lives in a global variable, singleton, or mutable module object, you create a risk of leakage between requests.

Bun has a built-in Cookie API for HTTP server routes: BunRequest includes cookies, and modified cookies are automatically applied to the response in routes. This is convenient for session cookies, logout, and refresh flows. [1]

But the cookie API does not solve security policy. OWASP session guidance emphasizes HTTPS for the entire session, the Secure cookie attribute, limited persistence, session lifecycle control, and reauthentication after risky events. [5]

For browser auth in Bun, sessions should be designed as server-side state: the cookie contains an opaque session id, while permissions, tenant, and risk metadata live in the session store. This gives you controlled logout, rotation, device tracking, and the ability to revoke a session without waiting for JWT expiry.

Session baseline

The cookie should be httpOnly, secure, have a correct sameSite, narrow path, clear TTL, and a server-side session store. Do not put sensitive user data directly into the cookie.

If you do not want to write the entire auth layer by hand, Hono and Elysia provide ready-made primitives. But ready-made middleware does not remove your responsibility for issuer, audience, scopes, tenant, and error contracts.

Hono jwt() verifies the token and puts the payload into context via c.get('jwtPayload'); the middleware looks for the Authorization header unless a cookie option is configured. [2]
Hono JWT Auth Middleware
Hono bearerAuth() works well for API tokens and provides verifyToken, custom errors, realm, prefix, and headerName. [3]
Hono Bearer Auth Middleware
Elysia has a JWT plugin for signing/verification in handlers and a Bearer plugin for extracting the bearer token. This fits naturally into the Elysia lifecycle. [6][7]
Elysia JWT and Bearer plugins

Summary

Built-in middleware is useful for parsing and basic verification. But tenant resolution, scopes, audit, and business authorization remain application design decisions.

In SaaS, the most common auth mistake does not look like “there is no token”. It looks like “a valid user was able to work inside someone else's tenant”. The reason is simple: the middleware took x-tenant-id from the request and did not verify that the subject actually has access to that tenant.

Tenant context must be derived from the credential or checked against server-side membership. If the user can switch tenants, a header or path param can be an input, but not an authority.

The correct flow is: verify credential, get subject, get allowed tenants/scopes, compare requested tenant against the allowed list, and attach the resolved tenant to context. If the tenant is not allowed, that is 403, not 401.

Do not use x-tenant-id as the source of truth.
Do not cache tenant permissions longer than your revocation model allows.
Log tenant id and subject id, but never the credential.
Write tests for cross-tenant access, not only for missing tokens.

Tenant must be resolved and verified by the server. A header or path param can be the user's request, but it is not proof of access.

Section multi-tenant-context screenshot

Incorrect auth errors create problems for clients, logs, and security reviews. In a Bun pipeline, it is better to define the contract upfront.

01

Credential is missing or invalid

Missing Authorization, malformed token, invalid signature, expired session, API key not found. The client should understand that authentication is required.

02

Credential is valid, but the action is forbidden

The subject is authenticated, but does not have tenant access, scope, role, or permission for the resource.

03

Auth abuse or rate limit

Brute force, too many invalid token attempts, API key abuse. Do not mix this with 401, because the client and monitoring should react differently.

04

Auth infrastructure failure

Session store, JWKS endpoint, Redis, or DB is unavailable. For some routes, fail closed is better; for public degraded flows, another policy may be needed.

Summary

A unified error format matters as much as the correct status code. The client, monitoring, and security tooling must see a stable contract.

Auth often becomes the heaviest middleware because it pulls in crypto, storage, tenant resolution, and audit. Optimization does not start with the algorithm; it starts with routing and caching policy.

Good: public/system routes outside the auth pipeline

/health, /ready, static assets, and public docs should not do token parsing or session lookup.

Careful: DB lookup for every JWT

If every JWT still goes to the DB for verification, ask whether JWT gives you any benefit over a session id. You may need a session/introspection model instead.

Good: short cache for stable auth metadata

Tenant membership, API key owner, and JWKS keys can be cached with a clear TTL and revocation story.

Careful: long-lived permissions cache

Permissions change. If the cache lives longer than the security expectation, a user may retain access after revoke.

Summary

Auth performance should not conflict with revocation. If you cache, define TTL, invalidation, and high-risk fallback immediately.

These mistakes are not specific to Bun only, but in Bun projects they often appear under the label of “we just quickly wrote thin middleware”.

Using jwt.decode() or reading the payload without signature verification.

Not checking exp, iss, aud, and algorithm for JWT.

Storing an access token in localStorage for a browser app without a clear XSS threat model.

Logging Authorization, cookies, refresh token, or the full API key.

Storing API keys in plaintext instead of hash + prefix lookup.

Issuing an API key without scopes, owner, environment, and rotation policy.

Treating x-tenant-id as an authoritative source of tenant context.

Returning 200 with { error: ... } for auth failures.

Mixing authentication, tenant membership, and resource authorization into one global middleware.

Review rule

If the middleware cannot be explained as a small state machine with 401/403/next, it is already too complex or taking responsibility for the wrong layer.

Before staging or a security review, go through this list. It is intentionally written as an engineering checklist, not generic advice.

Credential extraction happens once

One layer reads Authorization/cookies/API key and passes the result forward as typed context.

JWT verification is complete

Signature, algorithm, expiry, issuer, audience, subject, and clock skew policy are verified.

Session cookies are protected

httpOnly, secure, sameSite, narrow path, TTL, and HTTPS policy are defined.

API keys are not stored openly

The database stores only hash, key prefix, owner, scopes, last used, created/revoked metadata.

Tenant context is verified

Requested tenant is checked against subject membership/scopes instead of being accepted from a header as fact.

401 and 403 are separated

Missing/invalid credential returns 401; valid credential without permission returns 403.

Secrets are redacted

Logs, tracing, error reports, and analytics do not contain tokens, cookies, API keys, or PII.

A revocation story exists

JWT, sessions, and API keys have a clear model for revocation, rotation, and forced logout.

In Bun, it is easy to write a fast auth wrapper. It is harder to write auth middleware that does not mix authentication and authorization, does not leak between tenants, does not log secrets, does not create an unnecessary DB lookup on every request, and has a proper error contract.

JWT, sessions, and API keys are not competing as “best” and “worst” options. They solve different jobs. JWT is convenient for stateless access, sessions are better for browser UX and controlled logout, and API keys are built for integrations. In production, you often need a hybrid model, but with one unified audit and policy layer.

A good Bun auth pipeline looks simple: extract, verify, resolve subject, resolve tenant, attach context, continue or reject. Everything else should be explicitly designed, tested, and measured.

Is JWT better than sessions in Bun.js?

Not always. JWT is useful for stateless access and service-to-service scenarios, but revocation is harder. Sessions are a better fit for browser SaaS where you need controlled logout, secure cookies, and a session store. The choice depends on the client, threat model, and revocation requirements. [4][5]

Can I build auth middleware without Hono or Elysia?

Yes. Native `Bun.serve` lets you write auth as function composition around `Request`/`Response`. But you are responsible for context, errors, order, tests, and security policy.

Should middleware return 401 or 403?

`401` is for missing/invalid credentials: no token, expired session, invalid signature, API key not found. `403` is for a valid credential without permission, tenant access, or the required scope.

Where should tenant context be stored?

In request-scoped auth context after credential and membership verification. Do not trust `x-tenant-id` as the source of truth. A header or path param can be the requested tenant, but middleware must verify the subject has access to it.

Can tokens be logged for debugging?

No. Authorization headers, cookies, refresh tokens, and API keys should be redacted. Log request id, subject id, tenant id, auth method, and error class, but not the credential.

How should API keys be stored?

Show the full API key only once during creation, store a hash, keep a short prefix for lookup, and add owner, scopes, environment, created/last used/revoked metadata, and a rotation policy.

These sources confirm the behavior of the Bun Cookie API, Hono/Elysia auth middleware/plugins, and the security baseline for JWT and session management.

Reviewed: 14 May 2026Applies to: Bun 1.3.xApplies to: Bun.serve routesApplies to: Hono 4.xApplies to: Elysia 1.xApplies to: TypeScript APITested with: Bun CookieMapTested with: Hono JWT Auth MiddlewareTested with: Hono Bearer Auth MiddlewareTested with: Elysia JWT pluginTested with: Elysia Bearer plugin

PAS7 Studio can help design auth middleware for Bun, Hono, or Elysia: JWT/session/API-key model, tenant context, scopes, secure cookies, rotation, observability, and security review.

This is especially useful for SaaS products, internal platforms, B2B APIs, and migrations from Express/Fastify where auth behavior has already spread across middleware, guards, and helpers.

You are here02/05

Auth middleware in Bun.js: JWT, sessions, API keys, and multi-tenant context

Related Articles

ai-assistants

AI Assistant Development Cost in 2026: RAG Chatbots, CRM Integrations, Guardrails, and Support

A practical buyer guide to AI assistant development cost in 2026: prototypes, RAG chatbots, knowledge-base assistants, CRM and website integrations, guardrails, evaluations, monitoring, and support.

blogs

AI for landing page development: where it speeds up launches and where it hurts conversion

A practical research piece on using AI for landing page development: v0, Webflow AI, Builder.io, Framer-like builders, UX generation, copy, SEO, personalization, A/B testing, template risk, accessibility, security and technical debt.

growth

AI SEO / GEO in 2026: Your Next Customers Aren’t Humans — They’re Agents

Search is shifting from clicks to answers. Bots and AI agents crawl, cite, recommend, and increasingly buy. Learn what AI SEO / GEO means, why classic SEO is no longer enough, and how PAS7 Studio helps brands win visibility in the agentic web.

blogs

The most powerful Apple chip yet? M5 Pro and M5 Max are breaking records

A data-backed March 2026 analysis of Apple M5 Pro and M5 Max. We break down why these chips can credibly be called Apple's most powerful pro laptop silicon, how they compare with M4 Pro, M4 Max, M1 Pro, M1 Max, and how they stack up against Intel and AMD laptop rivals.

Professional development for your business

We create modern web solutions and bots for businesses. Learn how we can help you achieve your goals.