diff --git a/src/middleware/auth.js b/src/middleware/auth.js index 155874d..84f13e4 100644 --- a/src/middleware/auth.js +++ b/src/middleware/auth.js @@ -1,4 +1,3 @@ -import { auth } from 'express-oauth2-jwt-bearer'; import bcrypt from 'bcryptjs'; import { dbGet, dbRun } from '../db/database.js'; import { ApiResponse } from '../utils/helpers.js'; @@ -8,9 +7,10 @@ const SALT_ROUNDS = 12; // Auth0 configuration from environment const AUTH0_DOMAIN = process.env.AUTH0_DOMAIN || 'dev-t13zhs74oltgqtfx.us.auth0.com'; -const AUTH0_AUDIENCE = process.env.AUTH0_AUDIENCE || `https://${AUTH0_DOMAIN}/api/v2/`; -const AUTH0_CLIENT_ID = process.env.AUTH0_CLIENT_ID; -const AUTH0_CLIENT_SECRET = process.env.AUTH0_CLIENT_SECRET; + +// Simple in-memory cache for userinfo to reduce API calls +const userInfoCache = new Map(); +const CACHE_TTL = 5 * 60 * 1000; // 5 minutes /** * Hash a password (for API keys and local use) @@ -32,13 +32,72 @@ export const comparePassword = async (password, hash) => { }; /** - * Auth0 JWT validation middleware - * Validates the access token from Auth0 + * Fetch user info from Auth0 using the access token + * Works with both opaque tokens and JWTs */ -export const validateAccessToken = auth({ - issuerBaseURL: `https://${AUTH0_DOMAIN}`, - audience: AUTH0_AUDIENCE, -}); +async function fetchUserInfo(accessToken) { + // Check cache first + const cached = userInfoCache.get(accessToken); + if (cached && Date.now() - cached.timestamp < CACHE_TTL) { + return cached.data; + } + + const response = await fetch(`https://${AUTH0_DOMAIN}/userinfo`, { + headers: { + 'Authorization': `Bearer ${accessToken}` + } + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Auth0 userinfo failed: ${response.status} ${error}`); + } + + const userInfo = await response.json(); + + // Cache the result + userInfoCache.set(accessToken, { + data: userInfo, + timestamp: Date.now() + }); + + return userInfo; +} + +/** + * Validate access token and attach user info to request + * Works with both opaque tokens and JWTs from Auth0 + */ +export const validateAccessToken = async (req, res, next) => { + const authHeader = req.headers.authorization; + + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return res.status(401).json(ApiResponse(false, null, 'Authorization header required')); + } + + const accessToken = authHeader.substring(7); // Remove 'Bearer ' prefix + + try { + const userInfo = await fetchUserInfo(accessToken); + + // Attach to request + req.auth = { + payload: { + sub: userInfo.sub, + email: userInfo.email, + name: userInfo.name, + nickname: userInfo.nickname, + picture: userInfo.picture, + email_verified: userInfo.email_verified + } + }; + + next(); + } catch (error) { + console.error('Token validation error:', error.message); + return res.status(401).json(ApiResponse(false, null, 'Invalid or expired token')); + } +}; /** * Check if user exists in our database, create if not (first-time Auth0 login) @@ -46,7 +105,7 @@ export const validateAccessToken = auth({ */ export const ensureUserExists = async (req, res, next) => { try { - // Auth0 payload from JWT + // Auth0 payload from userinfo const auth0User = req.auth?.payload; if (!auth0User) { @@ -55,9 +114,9 @@ export const ensureUserExists = async (req, res, next) => { // Extract Auth0 user ID (sub claim) const auth0Id = auth0User.sub; - const email = auth0User.email || auth0User['https://moxiegen.client.guacamolebox.net/email']; - const name = auth0User.name || auth0User.nickname || auth0User['https://moxiegen.client.guacamolebox.net/name']; - const picture = auth0User.picture || auth0User['https://moxiegen.client.guacamolebox.net/picture']; + const email = auth0User.email; + const name = auth0User.name || auth0User.nickname; + const picture = auth0User.picture; // Check if user exists by auth0_id let user = await dbGet('SELECT * FROM users WHERE auth0_id = ?', [auth0Id]); @@ -119,7 +178,7 @@ export const ensureUserExists = async (req, res, next) => { /** * Combined Auth0 authentication middleware - * Validates JWT and ensures user exists in database + * Validates token via userinfo and ensures user exists in database */ export const authenticateToken = [validateAccessToken, ensureUserExists]; @@ -133,24 +192,20 @@ export const optionalAuth = async (req, res, next) => { if (!authHeader || !authHeader.startsWith('Bearer ')) { req.user = null; req.auth0 = null; + req.auth = null; return next(); } try { - // Try to validate the token - await new Promise((resolve, reject) => { - validateAccessToken(req, res, (err) => { - if (err) reject(err); - else resolve(); - }); + await validateAccessToken(req, res, (err) => { + if (err) throw err; }); - - // If valid, ensure user exists await ensureUserExists(req, res, next); } catch (error) { // Token invalid or missing - continue without user req.user = null; req.auth0 = null; + req.auth = null; next(); } }; @@ -220,6 +275,16 @@ setInterval(() => { } }, 60000); +// Clean up userinfo cache periodically +setInterval(() => { + const now = Date.now(); + for (const [token, cached] of userInfoCache.entries()) { + if (now - cached.timestamp > CACHE_TTL) { + userInfoCache.delete(token); + } + } +}, CACHE_TTL); + /** * Check if user has required scope/permission * @param {string} scope - Required scope