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:
@@ -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();
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user