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 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
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user