bulltrackers-module 1.0.597 → 1.0.598
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -519,6 +519,59 @@ const fetchUserVerificationData = async (firestore, userId) => {
|
|
|
519
519
|
}
|
|
520
520
|
};
|
|
521
521
|
|
|
522
|
+
/**
|
|
523
|
+
* Lookup CID by email (server-side only for security)
|
|
524
|
+
* Searches through all SignedInUsers verification documents to find matching email
|
|
525
|
+
* Returns only the CID and basic verification status - no email exposure
|
|
526
|
+
*/
|
|
527
|
+
const lookupCidByEmail = async (firestore, userEmail) => {
|
|
528
|
+
if (!userEmail) {
|
|
529
|
+
throw new Error('Email is required for lookup');
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
try {
|
|
533
|
+
// Normalize email for comparison
|
|
534
|
+
const normalizedEmail = String(userEmail).toLowerCase().trim();
|
|
535
|
+
|
|
536
|
+
// Get all SignedInUsers documents
|
|
537
|
+
const signedInUsersSnapshot = await firestore.collection('SignedInUsers').get();
|
|
538
|
+
|
|
539
|
+
// Search through each CID's verification document
|
|
540
|
+
for (const userDoc of signedInUsersSnapshot.docs) {
|
|
541
|
+
const verificationRef = userDoc.ref.collection('verification').doc('data');
|
|
542
|
+
const verificationDoc = await verificationRef.get();
|
|
543
|
+
|
|
544
|
+
if (verificationDoc.exists()) {
|
|
545
|
+
const verificationData = verificationDoc.data();
|
|
546
|
+
const emails = Array.isArray(verificationData.email)
|
|
547
|
+
? verificationData.email
|
|
548
|
+
: (verificationData.email ? [verificationData.email] : []);
|
|
549
|
+
|
|
550
|
+
// Case-insensitive email comparison
|
|
551
|
+
const normalizedEmails = emails.map(e => String(e).toLowerCase().trim());
|
|
552
|
+
if (normalizedEmails.includes(normalizedEmail)) {
|
|
553
|
+
// Found match - return only CID and verification status (no email)
|
|
554
|
+
return {
|
|
555
|
+
cid: verificationData.etoroCID || Number(userDoc.id),
|
|
556
|
+
accountSetupComplete: verificationData.accountSetupComplete || false,
|
|
557
|
+
etoroUsername: verificationData.etoroUsername,
|
|
558
|
+
displayName: verificationData.displayName,
|
|
559
|
+
photoURL: verificationData.photoURL,
|
|
560
|
+
verifiedAt: verificationData.verifiedAt,
|
|
561
|
+
setupCompletedAt: verificationData.setupCompletedAt,
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// No match found
|
|
568
|
+
return null;
|
|
569
|
+
} catch (error) {
|
|
570
|
+
console.error(`Error looking up CID by email: ${error}`);
|
|
571
|
+
throw error;
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
|
|
522
575
|
|
|
523
576
|
// 7. PI addition requests fetcher
|
|
524
577
|
// This is used to allow the user to request new PIs to add to the popular investor list to process data each day.
|
|
@@ -2244,6 +2297,7 @@ module.exports = {
|
|
|
2244
2297
|
pageCollection,
|
|
2245
2298
|
fetchPopularInvestorMasterList,
|
|
2246
2299
|
isDeveloper,
|
|
2300
|
+
lookupCidByEmail,
|
|
2247
2301
|
manageUserWatchlist,
|
|
2248
2302
|
fetchUserVerificationData,
|
|
2249
2303
|
requestPopularInvestorAddition,
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Firebase Auth Token Verification - How It Works
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This middleware verifies Firebase Authentication ID tokens server-side to securely extract user information (like email) without relying on client-provided data.
|
|
6
|
+
|
|
7
|
+
## Why This Is Important
|
|
8
|
+
|
|
9
|
+
**Without token verification:**
|
|
10
|
+
- Client sends email as query parameter: `GET /verification/lookup?email=user@example.com`
|
|
11
|
+
- Anyone could call this with any email address
|
|
12
|
+
- No way to verify the email actually belongs to the authenticated user
|
|
13
|
+
|
|
14
|
+
**With token verification:**
|
|
15
|
+
- Client sends Firebase ID token: `Authorization: Bearer <token>`
|
|
16
|
+
- Server verifies the token is valid and not expired
|
|
17
|
+
- Server extracts email from the verified token (cannot be spoofed)
|
|
18
|
+
- Only the authenticated user's email can be used
|
|
19
|
+
|
|
20
|
+
## How It Works
|
|
21
|
+
|
|
22
|
+
### 1. Frontend (Client-Side)
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
// User signs in with Firebase Auth
|
|
26
|
+
const user = await signInWithPopup(auth, googleProvider);
|
|
27
|
+
|
|
28
|
+
// Get the ID token (this is a JWT signed by Firebase)
|
|
29
|
+
const idToken = await user.getIdToken();
|
|
30
|
+
|
|
31
|
+
// Send token in Authorization header
|
|
32
|
+
fetch('/verification/lookup', {
|
|
33
|
+
headers: {
|
|
34
|
+
'Authorization': `Bearer ${idToken}`
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 2. Backend (Server-Side)
|
|
40
|
+
|
|
41
|
+
```javascript
|
|
42
|
+
// Middleware verifies the token
|
|
43
|
+
const decodedToken = await admin.auth().verifyIdToken(token);
|
|
44
|
+
|
|
45
|
+
// Extract email from verified token
|
|
46
|
+
const email = decodedToken.email; // This is guaranteed to be correct
|
|
47
|
+
|
|
48
|
+
// Use email for lookup
|
|
49
|
+
const result = await lookupCidByEmail(db, email);
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Token Structure
|
|
53
|
+
|
|
54
|
+
Firebase ID tokens are JWTs (JSON Web Tokens) that contain:
|
|
55
|
+
- `uid`: User's Firebase UID
|
|
56
|
+
- `email`: User's email address
|
|
57
|
+
- `email_verified`: Whether email is verified
|
|
58
|
+
- `name`: User's display name
|
|
59
|
+
- `picture`: User's profile picture URL
|
|
60
|
+
- `exp`: Expiration timestamp
|
|
61
|
+
- `iat`: Issued at timestamp
|
|
62
|
+
|
|
63
|
+
The token is **cryptographically signed by Firebase**, so:
|
|
64
|
+
- ✅ Cannot be forged
|
|
65
|
+
- ✅ Cannot be modified
|
|
66
|
+
- ✅ Expires automatically (1 hour default)
|
|
67
|
+
- ✅ Can be verified server-side
|
|
68
|
+
|
|
69
|
+
## Security Benefits
|
|
70
|
+
|
|
71
|
+
1. **Prevents Email Spoofing**: Users cannot lookup other users' data by providing a different email
|
|
72
|
+
2. **Automatic Expiration**: Tokens expire after 1 hour, requiring re-authentication
|
|
73
|
+
3. **Cryptographic Verification**: Token signature is verified against Firebase's public keys
|
|
74
|
+
4. **No Client Trust**: Server doesn't trust client-provided data, only verified tokens
|
|
75
|
+
|
|
76
|
+
## Usage
|
|
77
|
+
|
|
78
|
+
### In Routes
|
|
79
|
+
|
|
80
|
+
```javascript
|
|
81
|
+
const { requireFirebaseAuth } = require('../middleware/firebase_auth_middleware.js');
|
|
82
|
+
|
|
83
|
+
// Require authentication
|
|
84
|
+
router.get('/lookup', requireFirebaseAuth, async (req, res) => {
|
|
85
|
+
// req.firebaseUser.email is guaranteed to be from verified token
|
|
86
|
+
const email = req.firebaseUser.email;
|
|
87
|
+
// ... use email securely
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Optional Auth (for public routes)
|
|
92
|
+
|
|
93
|
+
```javascript
|
|
94
|
+
const { verifyFirebaseToken } = require('../middleware/firebase_auth_middleware.js');
|
|
95
|
+
|
|
96
|
+
// Optional authentication
|
|
97
|
+
router.get('/public', verifyFirebaseToken, async (req, res) => {
|
|
98
|
+
if (req.firebaseUser) {
|
|
99
|
+
// User is authenticated
|
|
100
|
+
} else {
|
|
101
|
+
// Public access
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Firebase Admin SDK Setup
|
|
107
|
+
|
|
108
|
+
The middleware automatically initializes Firebase Admin using:
|
|
109
|
+
1. **Cloud Functions**: Uses the function's service account automatically
|
|
110
|
+
2. **Local Development**: Uses `GOOGLE_APPLICATION_CREDENTIALS` environment variable
|
|
111
|
+
3. **Default Credentials**: Falls back to Application Default Credentials (ADC)
|
|
112
|
+
|
|
113
|
+
No manual configuration needed in Cloud Functions - it "just works"!
|
|
114
|
+
|
|
115
|
+
## Error Handling
|
|
116
|
+
|
|
117
|
+
- **Missing Token**: `req.firebaseUser` is `null`, request continues (routes can check)
|
|
118
|
+
- **Invalid Token**: `req.firebaseUser` is `null`, request continues
|
|
119
|
+
- **Expired Token**: `req.firebaseUser` is `null`, request continues
|
|
120
|
+
- **Required Auth**: `requireFirebaseAuth` returns 401 if token is invalid/missing
|
|
121
|
+
|
|
122
|
+
## Token Refresh
|
|
123
|
+
|
|
124
|
+
Firebase tokens expire after 1 hour. The frontend automatically refreshes tokens:
|
|
125
|
+
- Firebase SDK handles token refresh automatically
|
|
126
|
+
- `getIdToken()` always returns a valid token (refreshes if needed)
|
|
127
|
+
- No manual refresh logic needed
|
|
128
|
+
|
|
129
|
+
## Example Flow
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
1. User signs in → Firebase Auth creates session
|
|
133
|
+
2. Frontend calls getIdToken() → Gets current ID token
|
|
134
|
+
3. Frontend sends token in Authorization header
|
|
135
|
+
4. Backend middleware verifies token with Firebase Admin
|
|
136
|
+
5. Backend extracts email from verified token
|
|
137
|
+
6. Backend uses email for secure lookup
|
|
138
|
+
7. Backend returns result (no email exposure)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
This ensures the email used for lookup is **always** the authenticated user's email, with cryptographic proof.
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Firebase Auth Token Verification Middleware
|
|
3
|
+
* Verifies Firebase ID tokens and extracts user information
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Firebase Admin SDK is used server-side to verify tokens
|
|
7
|
+
// Note: This requires firebase-admin to be installed
|
|
8
|
+
let admin = null;
|
|
9
|
+
try {
|
|
10
|
+
admin = require('firebase-admin');
|
|
11
|
+
} catch (e) {
|
|
12
|
+
console.warn('[FirebaseAuth] firebase-admin not available. Token verification will be disabled.');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Initialize Firebase Admin if not already initialized
|
|
17
|
+
*/
|
|
18
|
+
function initializeFirebaseAdmin() {
|
|
19
|
+
if (!admin) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
// Check if already initialized
|
|
25
|
+
if (admin.apps.length > 0) {
|
|
26
|
+
return admin.app();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Initialize with default credentials (uses GOOGLE_APPLICATION_CREDENTIALS or default service account)
|
|
30
|
+
// In Cloud Functions, this automatically uses the function's service account
|
|
31
|
+
admin.initializeApp({
|
|
32
|
+
// Credentials are automatically loaded from environment
|
|
33
|
+
projectId: process.env.GCP_PROJECT_ID || 'stocks-12345'
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
return admin.app();
|
|
37
|
+
} catch (error) {
|
|
38
|
+
// If already initialized, that's fine
|
|
39
|
+
if (error.code === 'app/already-initialized') {
|
|
40
|
+
return admin.app();
|
|
41
|
+
}
|
|
42
|
+
console.error('[FirebaseAuth] Error initializing Firebase Admin:', error);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Middleware to verify Firebase Auth ID token
|
|
49
|
+
* Extracts email and UID from verified token and attaches to req.firebaseUser
|
|
50
|
+
*
|
|
51
|
+
* Usage:
|
|
52
|
+
* router.use('/protected-route', verifyFirebaseToken);
|
|
53
|
+
* // Then access req.firebaseUser.email and req.firebaseUser.uid
|
|
54
|
+
*/
|
|
55
|
+
const verifyFirebaseToken = async (req, res, next) => {
|
|
56
|
+
// Skip if Firebase Admin is not available
|
|
57
|
+
if (!admin) {
|
|
58
|
+
return next();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
// Get token from Authorization header
|
|
63
|
+
// Format: "Bearer <token>" or just "<token>"
|
|
64
|
+
const authHeader = req.headers.authorization;
|
|
65
|
+
|
|
66
|
+
if (!authHeader) {
|
|
67
|
+
// No token provided - continue without setting req.firebaseUser
|
|
68
|
+
// Routes can check for req.firebaseUser to determine if auth is required
|
|
69
|
+
return next();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Extract token (handle both "Bearer <token>" and just "<token>" formats)
|
|
73
|
+
const token = authHeader.startsWith('Bearer ')
|
|
74
|
+
? authHeader.substring(7)
|
|
75
|
+
: authHeader;
|
|
76
|
+
|
|
77
|
+
if (!token || token === 'null' || token === 'undefined') {
|
|
78
|
+
return next();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Initialize Firebase Admin if needed
|
|
82
|
+
const app = initializeFirebaseAdmin();
|
|
83
|
+
if (!app) {
|
|
84
|
+
console.warn('[FirebaseAuth] Cannot verify token - Firebase Admin not initialized');
|
|
85
|
+
return next();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Verify the token
|
|
89
|
+
const decodedToken = await admin.auth().verifyIdToken(token);
|
|
90
|
+
|
|
91
|
+
// Attach user info to request
|
|
92
|
+
req.firebaseUser = {
|
|
93
|
+
uid: decodedToken.uid,
|
|
94
|
+
email: decodedToken.email,
|
|
95
|
+
emailVerified: decodedToken.email_verified || false,
|
|
96
|
+
name: decodedToken.name,
|
|
97
|
+
picture: decodedToken.picture,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
next();
|
|
101
|
+
} catch (error) {
|
|
102
|
+
// Token verification failed
|
|
103
|
+
// Don't block the request - let individual routes decide if auth is required
|
|
104
|
+
console.warn('[FirebaseAuth] Token verification failed:', error.message);
|
|
105
|
+
req.firebaseUser = null;
|
|
106
|
+
next();
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Middleware that REQUIRES authentication
|
|
112
|
+
* Returns 401 if token is invalid or missing
|
|
113
|
+
*/
|
|
114
|
+
const requireFirebaseAuth = async (req, res, next) => {
|
|
115
|
+
// First verify the token
|
|
116
|
+
await verifyFirebaseToken(req, res, () => {
|
|
117
|
+
// Check if user was authenticated
|
|
118
|
+
if (!req.firebaseUser || !req.firebaseUser.email) {
|
|
119
|
+
return res.status(401).json({
|
|
120
|
+
success: false,
|
|
121
|
+
error: 'Authentication required. Please provide a valid Firebase ID token.'
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
next();
|
|
125
|
+
});
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
module.exports = {
|
|
129
|
+
verifyFirebaseToken,
|
|
130
|
+
requireFirebaseAuth,
|
|
131
|
+
initializeFirebaseAdmin,
|
|
132
|
+
};
|
|
@@ -1,8 +1,43 @@
|
|
|
1
1
|
const express = require('express');
|
|
2
|
-
const { initiateVerification, finalizeVerification, fetchUserVerificationData } = require('../helpers/data-fetchers/firestore.js');
|
|
2
|
+
const { initiateVerification, finalizeVerification, fetchUserVerificationData, lookupCidByEmail } = require('../helpers/data-fetchers/firestore.js');
|
|
3
|
+
const { requireFirebaseAuth } = require('../middleware/firebase_auth_middleware.js');
|
|
3
4
|
|
|
4
5
|
const router = express.Router();
|
|
5
6
|
|
|
7
|
+
// GET /verification/lookup
|
|
8
|
+
// Server-side email-to-CID lookup (prevents exposing other users' emails to client)
|
|
9
|
+
// Requires Firebase Auth token in Authorization header: "Bearer <token>"
|
|
10
|
+
// Email is extracted from the verified token (secure - cannot be spoofed)
|
|
11
|
+
router.get('/lookup', requireFirebaseAuth, async (req, res) => {
|
|
12
|
+
try {
|
|
13
|
+
const { db } = req.dependencies;
|
|
14
|
+
|
|
15
|
+
// Get email from verified Firebase token (secure - cannot be spoofed)
|
|
16
|
+
const userEmail = req.firebaseUser?.email;
|
|
17
|
+
|
|
18
|
+
if (!userEmail) {
|
|
19
|
+
return res.status(400).json({
|
|
20
|
+
success: false,
|
|
21
|
+
error: 'Email not found in authentication token'
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const result = await lookupCidByEmail(db, userEmail);
|
|
26
|
+
|
|
27
|
+
if (result) {
|
|
28
|
+
res.json({ success: true, data: result });
|
|
29
|
+
} else {
|
|
30
|
+
res.status(200).json({
|
|
31
|
+
success: false,
|
|
32
|
+
message: 'No verification data found for this email'
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error('[Verification Lookup] Error:', error);
|
|
37
|
+
res.status(500).json({ success: false, error: error.message });
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
6
41
|
// GET /verification/status
|
|
7
42
|
router.get('/status', async (req, res) => {
|
|
8
43
|
try {
|