Skip to main content

Auth middleware

src/auth/middleware/auth.middleware.ts runs first. It populates req.user for downstream guards and handlers.

Inputs (in priority order)

  1. Cookie session — better-auth-session cookie. Validated against the session table.
  2. Bearer token — Authorization: Bearer <jwt>. ID token verified against JWKS.
  3. No auth — req.user left undefined.

Both are accepted on every endpoint. The endpoint's guard decides what it requires.

const session = await auth.api.getSession({ headers: req.headers });
if (session?.user) {
req.user = {
id: session.user.id,
email: session.user.email,
role: session.user.role,
activeOrgId: session.session.activeOrganizationId,
};
req.session = session;
}

Bearer token path

if (!req.user && req.headers.authorization?.startsWith("Bearer ")) {
const token = req.headers.authorization.slice(7);
const claims = await jose.jwtVerify(token, jwks, {
issuer: config.authUrl + "/api/auth",
audience: clientId,
});
req.user = {
id: claims.payload.sub!,
email: claims.payload.email as string,
role: claims.payload["marketplace:role"] as string,
};
}

JWKS is fetched via createRemoteJWKSet — cached, respects rotation. Don't hardcode keys.

Guards downstream

req.user.role is what guards check. If middleware didn't run or auth failed, req.user is undefined and any guard that requires a role will throw 401 or 403.

Auth controller bypass

/api/auth/* routes use @Res() (no passthrough). They invoke better-auth's toNodeHandler, which writes the response directly. The auth middleware still runs on these routes — it just doesn't end up gating anything because /api/auth/* controllers have no guards.

Do not add @UseGuards or @UseInterceptors to AuthController methods. better-auth's response format is fixed; wrapping it would break clients.

OAuth subroutes

/api/auth/oauth2/* — better-auth's OAuth provider plugin handles these. The middleware sees them as ordinary /api/auth/* traffic and lets them through.

Common gotcha — cross-origin cookies

In production, the browser may strip the better-auth-session cookie on cross-origin requests if it's not configured SameSite=None; Secure. If getSession returns undefined despite the user being signed in:

  1. Check the response set the cookie with SameSite=None; Secure.
  2. Check FRONTEND_URL matches the actual origin (CORS rejection would also drop credentials).
  3. Confirm the request was actually sent with credentials: 'include'.

See Troubleshooting → CORS and cookies.