feat: implement Firebase ID token authentication for passenger telemetry and update related configurations

This commit is contained in:
2026-09-18 04:04:43 +03:00
parent 863ac603bc
commit b5c6b1973e
8 changed files with 206 additions and 14 deletions

View File

@@ -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
};

View File

@@ -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
);

View 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
};

View File

@@ -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));