Auth middleware
src/auth/middleware/auth.middleware.ts runs first. It populates req.user for downstream guards and handlers.
Inputs (in priority order)
- Cookie session —
better-auth-sessioncookie. Validated against thesessiontable. - Bearer token —
Authorization: Bearer <jwt>. ID token verified against JWKS. - No auth —
req.userleft undefined.
Both are accepted on every endpoint. The endpoint's guard decides what it requires.
Cookie session path
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:
- Check the response set the cookie with
SameSite=None; Secure. - Check
FRONTEND_URLmatches the actual origin (CORS rejection would also drop credentials). - Confirm the request was actually sent with
credentials: 'include'.