From 1321440cd06636b57a6e4c2fbe67c5352f2bae64 Mon Sep 17 00:00:00 2001 From: Eric Gullickson <16152721+ericgullickson@users.noreply.github.com> Date: Mon, 16 Feb 2026 09:36:32 -0600 Subject: [PATCH] chore: update auth plugin and admin guard for UUID (refs #212) Auth plugin now uses profile.id (UUID) as userContext.userId instead of raw JWT sub. Admin guard queries admin_users by user_profile_id. Auth0 Management API calls continue using auth0Sub from JWT. Co-Authored-By: Claude Opus 4.6 --- .../src/core/plugins/admin-guard.plugin.ts | 4 +-- backend/src/core/plugins/auth.plugin.ts | 32 +++++++++++-------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/backend/src/core/plugins/admin-guard.plugin.ts b/backend/src/core/plugins/admin-guard.plugin.ts index 066fa64..c8c13e1 100644 --- a/backend/src/core/plugins/admin-guard.plugin.ts +++ b/backend/src/core/plugins/admin-guard.plugin.ts @@ -58,9 +58,9 @@ const adminGuardPlugin: FastifyPluginAsync = async (fastify) => { // Check if user is in admin_users table and not revoked const query = ` - SELECT auth0_sub, email, role, revoked_at + SELECT id, user_profile_id, email, role, revoked_at FROM admin_users - WHERE auth0_sub = $1 AND revoked_at IS NULL + WHERE user_profile_id = $1 AND revoked_at IS NULL LIMIT 1 `; diff --git a/backend/src/core/plugins/auth.plugin.ts b/backend/src/core/plugins/auth.plugin.ts index d13355c..4b3a93d 100644 --- a/backend/src/core/plugins/auth.plugin.ts +++ b/backend/src/core/plugins/auth.plugin.ts @@ -121,11 +121,14 @@ const authPlugin: FastifyPluginAsync = async (fastify) => { try { await request.jwtVerify(); - const userId = request.user?.sub; - if (!userId) { + // Two identifiers: auth0Sub (external, for Auth0 API) and userId (internal UUID, for all DB operations) + const auth0Sub = request.user?.sub; + if (!auth0Sub) { throw new Error('Missing user ID in JWT'); } + let userId: string = auth0Sub; // Default to auth0Sub; overwritten with UUID after profile load + // Get or create user profile from database let email = request.user?.email; let displayName: string | undefined; @@ -137,34 +140,35 @@ const authPlugin: FastifyPluginAsync = async (fastify) => { // If JWT doesn't have email, fetch from Auth0 Management API if (!email || email.includes('@unknown.local')) { try { - const auth0User = await auth0ManagementClient.getUser(userId); + const auth0User = await auth0ManagementClient.getUser(auth0Sub); if (auth0User.email) { email = auth0User.email; emailVerified = auth0User.emailVerified; logger.info('Fetched email from Auth0 Management API', { - userId: userId.substring(0, 8) + '...', + userId: auth0Sub.substring(0, 8) + '...', hasEmail: true, }); } } catch (auth0Error) { logger.warn('Failed to fetch user from Auth0 Management API', { - userId: userId.substring(0, 8) + '...', + userId: auth0Sub.substring(0, 8) + '...', error: auth0Error instanceof Error ? auth0Error.message : 'Unknown error', }); } } // Get or create profile with correct email - const profile = await profileRepo.getOrCreate(userId, { - email: email || `${userId}@unknown.local`, + const profile = await profileRepo.getOrCreate(auth0Sub, { + email: email || `${auth0Sub}@unknown.local`, displayName: request.user?.name || request.user?.nickname, }); + userId = profile.id; // If profile has placeholder email but we now have real email, update it if (profile.email.includes('@unknown.local') && email && !email.includes('@unknown.local')) { - await profileRepo.updateEmail(userId, email); + await profileRepo.updateEmail(auth0Sub, email); logger.info('Updated profile with correct email from Auth0', { - userId: userId.substring(0, 8) + '...', + userId: auth0Sub.substring(0, 8) + '...', }); } @@ -178,18 +182,18 @@ const authPlugin: FastifyPluginAsync = async (fastify) => { // Sync email verification status from Auth0 if needed if (!emailVerified) { try { - const isVerifiedInAuth0 = await auth0ManagementClient.checkEmailVerified(userId); + const isVerifiedInAuth0 = await auth0ManagementClient.checkEmailVerified(auth0Sub); if (isVerifiedInAuth0 && !profile.emailVerified) { - await profileRepo.updateEmailVerified(userId, true); + await profileRepo.updateEmailVerified(auth0Sub, true); emailVerified = true; logger.info('Synced email verification status from Auth0', { - userId: userId.substring(0, 8) + '...', + userId: auth0Sub.substring(0, 8) + '...', }); } } catch (syncError) { // Don't fail auth if sync fails, just log logger.warn('Failed to sync email verification status', { - userId: userId.substring(0, 8) + '...', + userId: auth0Sub.substring(0, 8) + '...', error: syncError instanceof Error ? syncError.message : 'Unknown error', }); } @@ -197,7 +201,7 @@ const authPlugin: FastifyPluginAsync = async (fastify) => { } catch (profileError) { // Log but don't fail auth if profile fetch fails logger.warn('Failed to fetch user profile', { - userId: userId.substring(0, 8) + '...', + userId: auth0Sub.substring(0, 8) + '...', error: profileError instanceof Error ? profileError.message : 'Unknown error', }); // Fall back to JWT email if available