Fix auth: validate tokens via Auth0 userinfo endpoint (supports opaque tokens)

This commit is contained in:
Z User 2026-03-27 23:31:14 +00:00
parent 67edb02b1f
commit f95821c08d

View File

@ -1,4 +1,3 @@
import { auth } from 'express-oauth2-jwt-bearer';
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
import { dbGet, dbRun } from '../db/database.js'; import { dbGet, dbRun } from '../db/database.js';
import { ApiResponse } from '../utils/helpers.js'; import { ApiResponse } from '../utils/helpers.js';
@ -8,9 +7,10 @@ const SALT_ROUNDS = 12;
// Auth0 configuration from environment // Auth0 configuration from environment
const AUTH0_DOMAIN = process.env.AUTH0_DOMAIN || 'dev-t13zhs74oltgqtfx.us.auth0.com'; 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; // Simple in-memory cache for userinfo to reduce API calls
const AUTH0_CLIENT_SECRET = process.env.AUTH0_CLIENT_SECRET; const userInfoCache = new Map();
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
/** /**
* Hash a password (for API keys and local use) * Hash a password (for API keys and local use)
@ -32,13 +32,72 @@ export const comparePassword = async (password, hash) => {
}; };
/** /**
* Auth0 JWT validation middleware * Fetch user info from Auth0 using the access token
* Validates the access token from Auth0 * Works with both opaque tokens and JWTs
*/ */
export const validateAccessToken = auth({ async function fetchUserInfo(accessToken) {
issuerBaseURL: `https://${AUTH0_DOMAIN}`, // Check cache first
audience: AUTH0_AUDIENCE, 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) * 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) => { export const ensureUserExists = async (req, res, next) => {
try { try {
// Auth0 payload from JWT // Auth0 payload from userinfo
const auth0User = req.auth?.payload; const auth0User = req.auth?.payload;
if (!auth0User) { if (!auth0User) {
@ -55,9 +114,9 @@ export const ensureUserExists = async (req, res, next) => {
// Extract Auth0 user ID (sub claim) // Extract Auth0 user ID (sub claim)
const auth0Id = auth0User.sub; const auth0Id = auth0User.sub;
const email = auth0User.email || auth0User['https://moxiegen.client.guacamolebox.net/email']; const email = auth0User.email;
const name = auth0User.name || auth0User.nickname || auth0User['https://moxiegen.client.guacamolebox.net/name']; const name = auth0User.name || auth0User.nickname;
const picture = auth0User.picture || auth0User['https://moxiegen.client.guacamolebox.net/picture']; const picture = auth0User.picture;
// Check if user exists by auth0_id // Check if user exists by auth0_id
let user = await dbGet('SELECT * FROM users WHERE auth0_id = ?', [auth0Id]); 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 * 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]; export const authenticateToken = [validateAccessToken, ensureUserExists];
@ -133,24 +192,20 @@ export const optionalAuth = async (req, res, next) => {
if (!authHeader || !authHeader.startsWith('Bearer ')) { if (!authHeader || !authHeader.startsWith('Bearer ')) {
req.user = null; req.user = null;
req.auth0 = null; req.auth0 = null;
req.auth = null;
return next(); return next();
} }
try { try {
// Try to validate the token await validateAccessToken(req, res, (err) => {
await new Promise((resolve, reject) => { if (err) throw err;
validateAccessToken(req, res, (err) => {
if (err) reject(err);
else resolve();
});
}); });
// If valid, ensure user exists
await ensureUserExists(req, res, next); await ensureUserExists(req, res, next);
} catch (error) { } catch (error) {
// Token invalid or missing - continue without user // Token invalid or missing - continue without user
req.user = null; req.user = null;
req.auth0 = null; req.auth0 = null;
req.auth = null;
next(); next();
} }
}; };
@ -220,6 +275,16 @@ setInterval(() => {
} }
}, 60000); }, 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 * Check if user has required scope/permission
* @param {string} scope - Required scope * @param {string} scope - Required scope