diff --git a/apps/backend/src/middlewares/require-auth.middleware.ts b/apps/backend/src/middlewares/require-auth.middleware.ts index ab4e306..310d377 100644 --- a/apps/backend/src/middlewares/require-auth.middleware.ts +++ b/apps/backend/src/middlewares/require-auth.middleware.ts @@ -2,6 +2,26 @@ import { db } from '@/lib/prisma'; import { supabase } from '@/lib/supabase'; import type { AppEnv } from '@/types/hono'; import type { MiddlewareHandler } from 'hono'; +import { createRemoteJWKSet, jwtVerify } from 'jose'; + +const JWKS_CACHE_TTL_MS = 5 * 60 * 1000; + +let jwks: ReturnType | null = null; +let jwksCacheTime = 0; + +async function getJWKS() { + const now = Date.now(); + if (!jwks || now - jwksCacheTime > JWKS_CACHE_TTL_MS) { + const supabaseUrl = process.env.SUPABASE_URL ?? process.env.VITE_SUPABASE_URL; + if (!supabaseUrl) { + throw new Error('SUPABASE_URL not configured'); + } + const jwksUrl = new URL('/auth/v1/.well-known/jwks.json', supabaseUrl).toString(); + jwks = createRemoteJWKSet(new URL(jwksUrl)); + jwksCacheTime = now; + } + return jwks; +} function getBearerToken(value: string | undefined): string | null { if (!value) return null; @@ -17,23 +37,34 @@ export const requireAuth: MiddlewareHandler = async (c, next) => { return c.json({ message: 'Missing bearer token.' }, 403); } - const { data, error } = await supabase.auth.getUser(token); + let supabaseUserId: string; + let email: string | undefined; - if (error || !data.user) { - return c.json({ message: 'Invalid or expired token.' }, 403); + try { + const jwksSet = await getJWKS(); + const { payload } = await jwtVerify(token, jwksSet, { + algorithms: ['ES256'], + }); + + supabaseUserId = payload.sub as string; + email = payload.email as string | undefined; + } catch { + const { data, error } = await supabase.auth.getUser(token); + + if (error || !data.user) { + return c.json({ message: 'Invalid or expired token.' }, 403); + } + + supabaseUserId = data.user.id; + email = data.user.email?.toLowerCase(); } - const email = data.user.email?.toLowerCase(); - if (!email) { return c.json({ message: 'Auth user does not contain email.' }, 403); } - const appUser = await db.user.findFirst({ - where: { - supabaseUserId: data.user.id, - email, - }, + const appUser = await db.user.findUnique({ + where: { supabaseUserId }, select: { id: true }, }); @@ -41,7 +72,7 @@ export const requireAuth: MiddlewareHandler = async (c, next) => { return c.json({ message: 'User not provisioned in backend.' }, 403); } - c.set('authUser', data.user); c.set('appUserId', appUser.id); + await next(); }; diff --git a/bun.lock b/bun.lock index a71ff8e..00c2f5f 100644 --- a/bun.lock +++ b/bun.lock @@ -5,6 +5,7 @@ "": { "name": "bun-monorepo", "dependencies": { + "jose": "^6.2.2", "zod": "^4.3.6", }, "devDependencies": { diff --git a/package.json b/package.json index d9e361b..c2be90e 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "packages/*" ], "dependencies": { + "jose": "^6.2.2", "zod": "^4.3.6" }, "scripts": {