perf: validate JWT locally instead of calling Supabase API

- Use jose library for ES256 JWT verification with cached JWKS
- Fallback to Supabase only if JWT verification fails
- Use findUnique instead of findFirst for DB lookup
- Estimated improvement: ~450ms -> ~5ms per request
This commit is contained in:
Jose Selesan
2026-04-13 17:50:52 -03:00
parent 1d09716492
commit b7c303377a
3 changed files with 44 additions and 11 deletions

View File

@@ -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<typeof createRemoteJWKSet> | 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<AppEnv> = 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<AppEnv> = 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();
};

View File

@@ -5,6 +5,7 @@
"": {
"name": "bun-monorepo",
"dependencies": {
"jose": "^6.2.2",
"zod": "^4.3.6",
},
"devDependencies": {

View File

@@ -6,6 +6,7 @@
"packages/*"
],
"dependencies": {
"jose": "^6.2.2",
"zod": "^4.3.6"
},
"scripts": {