Fix auth: validate tokens via Auth0 userinfo endpoint (supports opaque tokens)
This commit is contained in:
parent
67edb02b1f
commit
f95821c08d
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user