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 { 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