feat: implement Firebase ID token authentication for passenger telemetry and update related configurations
This commit is contained in:
@@ -24,6 +24,18 @@ function getFirebaseConfig(env = process.env) {
|
||||
|
||||
let messagingSingleton;
|
||||
|
||||
function getFirebaseApp(config = getFirebaseConfig()) {
|
||||
// Keep Firebase Admin lazy: deployments that do not receive passenger telemetry
|
||||
// still run without local ADC credentials.
|
||||
const { applicationDefault, getApps, initializeApp } = require('firebase-admin/app');
|
||||
|
||||
const existingApp = getApps()[0];
|
||||
return existingApp || initializeApp({
|
||||
credential: applicationDefault(),
|
||||
...(config.projectId ? { projectId: config.projectId } : {})
|
||||
});
|
||||
}
|
||||
|
||||
function createFirebaseMessaging(config = getFirebaseConfig()) {
|
||||
if (!config.enabled) {
|
||||
return null;
|
||||
@@ -31,16 +43,8 @@ function createFirebaseMessaging(config = getFirebaseConfig()) {
|
||||
|
||||
// Keep Firebase optional for disabled/local environments. These modular
|
||||
// imports are evaluated only when an enabled notification is actually sent.
|
||||
const { applicationDefault, getApps, initializeApp } = require('firebase-admin/app');
|
||||
const { getMessaging } = require('firebase-admin/messaging');
|
||||
|
||||
const existingApp = getApps()[0];
|
||||
const app = existingApp || initializeApp({
|
||||
credential: applicationDefault(),
|
||||
...(config.projectId ? { projectId: config.projectId } : {})
|
||||
});
|
||||
|
||||
return getMessaging(app);
|
||||
return getMessaging(getFirebaseApp(config));
|
||||
}
|
||||
|
||||
function getFirebaseMessaging(config = getFirebaseConfig()) {
|
||||
@@ -55,11 +59,18 @@ function getFirebaseMessaging(config = getFirebaseConfig()) {
|
||||
return messagingSingleton;
|
||||
}
|
||||
|
||||
function createFirebaseAuth(config = getFirebaseConfig()) {
|
||||
const { getAuth } = require('firebase-admin/auth');
|
||||
return getAuth(getFirebaseApp(config));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
DEFAULT_FCM_TOPIC,
|
||||
DEFAULT_FCM_ANDROID_CHANNEL_ID,
|
||||
parseBoolean,
|
||||
getFirebaseConfig,
|
||||
getFirebaseApp,
|
||||
createFirebaseAuth,
|
||||
createFirebaseMessaging,
|
||||
getFirebaseMessaging
|
||||
};
|
||||
|
||||
@@ -77,9 +77,15 @@ try {
|
||||
logger.warn('Upload routes disabled - multer not installed. Run: npm install multer');
|
||||
}
|
||||
|
||||
// API routes with API key authentication and rate limiting
|
||||
// Passenger telemetry has its own Firebase ID-token authentication inside apiRoutes.
|
||||
// All other API routes keep the existing API-key/custom-JWT contract.
|
||||
app.use('/api/v1',
|
||||
authenticateApiKey,
|
||||
(req, res, next) => {
|
||||
if (req.path === '/passenger-telemetry/ingest') {
|
||||
return next();
|
||||
}
|
||||
return authenticateApiKey(req, res, next);
|
||||
},
|
||||
createRateLimiter(1000, 3600000), // 1000 requests per hour
|
||||
apiRoutes
|
||||
);
|
||||
|
||||
53
backend/src/middleware/passengerTelemetryAuth.js
Normal file
53
backend/src/middleware/passengerTelemetryAuth.js
Normal file
@@ -0,0 +1,53 @@
|
||||
const logger = require('../config/logger');
|
||||
const { createFirebaseAuth } = require('../config/firebase');
|
||||
|
||||
function getBearerToken(req) {
|
||||
const authorization = typeof req.get === 'function'
|
||||
? req.get('Authorization')
|
||||
: req.headers?.authorization;
|
||||
const match = /^Bearer\s+([^\s]+)$/i.exec(authorization || '');
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
function createPassengerTelemetryAuthenticator({
|
||||
getFirebaseAuth = createFirebaseAuth,
|
||||
authLogger = logger
|
||||
} = {}) {
|
||||
return async (req, res, next) => {
|
||||
const token = getBearerToken(req);
|
||||
if (!token) {
|
||||
return res.status(401).json({ error: 'Access token required' });
|
||||
}
|
||||
|
||||
let firebaseAuth;
|
||||
try {
|
||||
firebaseAuth = getFirebaseAuth();
|
||||
} catch (error) {
|
||||
authLogger.error('Passenger telemetry authentication is unavailable', {
|
||||
firebaseCode: error.code || 'unknown'
|
||||
});
|
||||
return res.status(503).json({ error: 'Passenger telemetry authentication is unavailable' });
|
||||
}
|
||||
|
||||
try {
|
||||
// Verify the Firebase ID token, including revocation. The decoded token and UID are
|
||||
// deliberately discarded: telemetry identity is the installation-scoped sessionId only.
|
||||
await firebaseAuth.verifyIdToken(token, true);
|
||||
req.passengerTelemetryAuthenticated = true;
|
||||
return next();
|
||||
} catch (error) {
|
||||
authLogger.warn('Passenger telemetry token was rejected', {
|
||||
firebaseCode: error.code || 'unknown'
|
||||
});
|
||||
return res.status(403).json({ error: 'Invalid or expired token' });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const authenticatePassengerTelemetry = createPassengerTelemetryAuthenticator();
|
||||
|
||||
module.exports = {
|
||||
createPassengerTelemetryAuthenticator,
|
||||
authenticatePassengerTelemetry,
|
||||
getBearerToken
|
||||
};
|
||||
@@ -12,6 +12,7 @@ const syncController = require('../controllers/syncController');
|
||||
const telemetryController = require('../controllers/telemetryController');
|
||||
const passengerTelemetryController = require('../controllers/passengerTelemetryController');
|
||||
const { createPassengerTelemetryRateLimiter } = require('../middleware/passengerTelemetryRateLimiter');
|
||||
const { authenticatePassengerTelemetry } = require('../middleware/passengerTelemetryAuth');
|
||||
const { authenticateToken, requireAdmin } = require('../middleware/auth');
|
||||
const { resolveCity } = require('../middleware/city');
|
||||
const qualityController = require('../controllers/qualityController');
|
||||
@@ -78,6 +79,7 @@ router.post('/telemetry/ingest', telemetryController.ingest.bind(telemetryContro
|
||||
router.post(
|
||||
'/passenger-telemetry/ingest',
|
||||
createPassengerTelemetryRateLimiter(),
|
||||
authenticatePassengerTelemetry,
|
||||
passengerTelemetryController.ingest.bind(passengerTelemetryController)
|
||||
);
|
||||
router.get('/realtime/routes/:routeId/vehicles', telemetryController.getRouteLiveVehicles.bind(telemetryController));
|
||||
|
||||
Reference in New Issue
Block a user