diff --git a/src/routes/authRoutes.js b/src/routes/authRoutes.js index 170252d..9c21521 100644 --- a/src/routes/authRoutes.js +++ b/src/routes/authRoutes.js @@ -1184,4 +1184,122 @@ router.post( } ); +// POST /auth/validate-token +// Endpoint for other services (like BuySellService) to validate JWT tokens +// This allows centralized token validation +router.post('/validate-token', async (req, res) => { + try { + const { token } = req.body; + + if (!token) { + return res.status(400).json({ + valid: false, + error: 'Token is required' + }); + } + + // Use the auth middleware logic to validate token + const jwt = require('jsonwebtoken'); + const { getKeySecret, getAllKeys, validateTokenClaims } = require('../services/jwtKeys'); + const db = require('../db'); + + // Decode token to get key ID from header + let decoded; + try { + decoded = jwt.decode(token, { complete: true }); + if (!decoded || !decoded.header) { + return res.json({ valid: false, error: 'Invalid token format' }); + } + } catch (err) { + return res.json({ valid: false, error: 'Invalid token' }); + } + + // Get secret for the key ID (if present) or try all keys + const keyId = decoded.header.kid; + let payload = null; + let verified = false; + + if (keyId) { + const secret = getKeySecret(keyId); + if (secret) { + try { + payload = jwt.verify(token, secret); + verified = true; + } catch (err) { + return res.json({ valid: false, error: 'Invalid or expired token' }); + } + } + } + + // If key ID not found or not specified, try all keys (for rotation support) + if (!verified) { + const allKeys = getAllKeys(); + for (const [kid, keySecret] of Object.entries(allKeys)) { + try { + payload = jwt.verify(token, keySecret); + verified = true; + break; + } catch (err) { + continue; + } + } + } + + if (!verified || !payload) { + return res.json({ valid: false, error: 'Invalid or expired token' }); + } + + // Validate JWT claims (iss, aud, exp, iat, nbf) + const claimsValidation = validateTokenClaims(payload); + if (!claimsValidation.valid) { + return res.json({ valid: false, error: 'Invalid token claims' }); + } + + // Validate token_version to ensure token hasn't been invalidated by logout-all-devices + try { + const { rows } = await db.query( + `SELECT COALESCE(token_version, 1) as token_version FROM users WHERE id = $1`, + [payload.sub] + ); + + if (rows.length === 0) { + return res.json({ valid: false, error: 'User not found' }); + } + + const userTokenVersion = rows[0].token_version; + const tokenVersion = payload.token_version || 1; + + // If token version doesn't match, token has been invalidated + if (tokenVersion !== userTokenVersion) { + return res.json({ valid: false, error: 'Token has been invalidated' }); + } + } catch (dbErr) { + // Handle missing token_version column gracefully + if (dbErr.code === '42703' && dbErr.message && dbErr.message.includes('token_version')) { + console.warn('token_version column not found in database, skipping version check'); + // Continue without token version validation + } else { + console.error('Error validating token version:', dbErr); + return res.status(500).json({ valid: false, error: 'Internal server error' }); + } + } + + // Token is valid - return user info + return res.json({ + valid: true, + payload: { + sub: payload.sub, + role: payload.role, + user_type: payload.user_type, + phone_number: payload.phone_number, + token_version: payload.token_version || 1, + high_assurance: payload.high_assurance || false, + }, + }); + } catch (err) { + console.error('validate-token error:', err); + return res.status(500).json({ valid: false, error: 'Internal server error' }); + } +}); + module.exports = router;