everydaycats commited on
Commit
afeb8cb
·
verified ·
1 Parent(s): ca39894

Update app.js

Browse files
Files changed (1) hide show
  1. app.js +43 -112
app.js CHANGED
@@ -1,4 +1,3 @@
1
-
2
  const express = require('express');
3
  const admin = require('firebase-admin');
4
  const jwt = require('jsonwebtoken');
@@ -10,18 +9,11 @@ const app = express();
10
  app.use(bodyParser.json());
11
 
12
  // ---------------------------------------------------------
13
- // 1. STATE MANAGEMENT (In-Memory DB)
14
  // ---------------------------------------------------------
15
-
16
- // Store temporary "proj_" redemption keys.
17
- // Structure: { "proj_xyz": { uid: "...", projectId: "...", expiresAt: timestamp } }
18
  const tempKeys = new Map();
19
-
20
- // Store JWT Secrets (Hydrated Cache).
21
- // Structure: { "uid:projectId": { secret: "...", lastAccessed: timestamp } }
22
  const activeSessions = new Map();
23
-
24
- let db = null; // Firebase DB Reference
25
 
26
  // ---------------------------------------------------------
27
  // 2. FIREBASE INITIALIZATION
@@ -38,20 +30,17 @@ try {
38
  db = admin.database();
39
  console.log("🔥 Firebase Connected & StateManager Linked");
40
  } else {
41
- console.warn("⚠️ Memory-Only mode (No Firebase JSON provided).");
42
  }
43
  } catch (e) {
44
  console.error("Firebase Init Error:", e);
45
  }
46
 
47
  // ---------------------------------------------------------
48
- // 3. HELPER FUNCTIONS & MIDDLEWARE
49
  // ---------------------------------------------------------
50
-
51
- // Middleware to verify Firebase ID Token (for /key and /nullify)
52
- // Includes a Debug Bypass variable
53
  const verifyFirebaseUser = async (req, res, next) => {
54
- const debugMode = process.env.DEBUG_NO_AUTH === 'true'; // Set to true in .env for testing without valid tokens
55
 
56
  if (debugMode) {
57
  req.user = { uid: "debug_user_001" };
@@ -71,7 +60,6 @@ const verifyFirebaseUser = async (req, res, next) => {
71
  req.user = decodedToken;
72
  next();
73
  } else {
74
- // Fallback for memory-only mode without validation
75
  req.user = { uid: "memory_user" };
76
  next();
77
  }
@@ -80,24 +68,18 @@ const verifyFirebaseUser = async (req, res, next) => {
80
  }
81
  };
82
 
83
- // Helper to fetch Secret (Memory -> DB -> 404)
84
  async function getSessionSecret(uid, projectId) {
85
  const cacheKey = `${uid}:${projectId}`;
86
-
87
- // 1. Check Memory
88
  if (activeSessions.has(cacheKey)) {
89
  const session = activeSessions.get(cacheKey);
90
- session.lastAccessed = Date.now(); // Update access time for cleanup
91
  return session.secret;
92
  }
93
-
94
- // 2. Hydrate from DB
95
  if (db) {
96
  try {
97
  const snapshot = await db.ref(`plugin_oauth/${uid}/${projectId}`).once('value');
98
  if (snapshot.exists()) {
99
  const secret = snapshot.val();
100
- // Store in memory for next time
101
  activeSessions.set(cacheKey, { secret, lastAccessed: Date.now() });
102
  console.log(`💧 Hydrated secret for ${cacheKey} from DB`);
103
  return secret;
@@ -106,8 +88,6 @@ async function getSessionSecret(uid, projectId) {
106
  console.error("DB Read Error:", err);
107
  }
108
  }
109
-
110
- // 3. Not found
111
  return null;
112
  }
113
 
@@ -115,15 +95,12 @@ async function getSessionSecret(uid, projectId) {
115
  // 4. ENDPOINTS
116
  // ---------------------------------------------------------
117
 
118
- // Endpoint: /key
119
- // Generates a temporary 'proj_' token. Expects Firebase Auth.
120
  app.post('/key', verifyFirebaseUser, (req, res) => {
121
  const { projectId } = req.body;
122
  if (!projectId) return res.status(400).json({ error: 'projectId required' });
123
 
124
  const key = `proj_${uuidv4().replace(/-/g, '')}`;
125
 
126
- // Store in memory (valid for 5 minutes)
127
  tempKeys.set(key, {
128
  uid: req.user.uid,
129
  projectId: projectId,
@@ -134,8 +111,6 @@ app.post('/key', verifyFirebaseUser, (req, res) => {
134
  res.json({ key, expiresIn: 300 });
135
  });
136
 
137
- // Endpoint: /redeem
138
- // Exchanges 'proj_' key for a JWT.
139
  app.post('/redeem', async (req, res) => {
140
  const { key } = req.body;
141
 
@@ -144,88 +119,82 @@ app.post('/redeem', async (req, res) => {
144
  }
145
 
146
  const data = tempKeys.get(key);
147
-
148
- // Generate a unique secret for this session/project
149
  const sessionSecret = uuidv4();
150
 
151
- // Create JWT
152
  const token = jwt.sign(
153
  { uid: data.uid, projectId: data.projectId },
154
  sessionSecret,
155
- { expiresIn: '7d' } // Plugin token validity
156
  );
157
 
158
  const cacheKey = `${data.uid}:${data.projectId}`;
159
 
160
- // 1. Store in Memory
161
  activeSessions.set(cacheKey, { secret: sessionSecret, lastAccessed: Date.now() });
162
 
163
- // 2. Store in Firebase (Persist)
164
  if (db) {
165
  await db.ref(`plugin_oauth/${data.uid}/${data.projectId}`).set(sessionSecret);
166
  }
167
 
168
- // Burn the redemption key (One-time use)
169
  tempKeys.delete(key);
170
-
171
  console.log(`🚀 Redeemed JWT for ${cacheKey}`);
172
  res.json({ token });
173
  });
174
 
175
- // Endpoint: /poll
176
- // Verifies JWT using the stored secret and forwards to external server.
177
  app.post('/poll', async (req, res) => {
178
  const { token, payload: clientPayload } = req.body;
179
 
180
  if (!token) return res.status(400).json({ error: 'Token required' });
181
 
182
- // Decode without verification first to find WHO it is
183
  const decoded = jwt.decode(token);
184
  if (!decoded || !decoded.uid || !decoded.projectId) {
185
  return res.status(401).json({ error: 'Malformed token' });
186
  }
187
 
188
- // Fetch secret (Memory -> DB -> 404)
189
  const secret = await getSessionSecret(decoded.uid, decoded.projectId);
190
 
191
  if (!secret) {
192
  return res.status(404).json({ error: 'Session revoked or not found' });
193
  }
194
 
195
- // Verify signature
196
  try {
197
- jwt.verify(token, secret);
198
- const user = decoded.uid;
 
 
 
 
 
199
 
200
- const project = decoded.projectId;
201
-
202
- // console.log("user: ",user," project: ", project);
 
203
 
204
- // If valid, post to external server
205
- const externalUrl = process.env.EXTERNAL_SERVER_URL || 'https://httpbin.org/post'; // Default for testing
206
 
207
  try {
208
  const response = await axios.post(externalUrl, {
209
- //user: decoded.uid,
210
- projectId: decoded.projectId,
211
- // data: clientPayload
212
  });
213
 
214
- // console.log(response.data);
215
-
216
  return res.json({ status: 'success', externalResponse: response.data });
217
  } catch (extError) {
218
  return res.status(502).json({ error: 'External server error' });
219
  }
220
 
221
  } catch (err) {
 
 
 
222
  return res.status(403).json({ error: 'Invalid Token Signature' });
223
  }
224
  });
225
 
226
- // Endpoint: /cleanup
227
- // Memory management: Clears old JWT secrets from RAM that haven't been used recently.
228
- // Does NOT delete from Firebase.
229
  app.get('/cleanup', (req, res) => {
230
  const THRESHOLD = 1000 * 60 * 60; // 1 Hour
231
  const now = Date.now();
@@ -238,7 +207,6 @@ app.get('/cleanup', (req, res) => {
238
  }
239
  }
240
 
241
- // Also clean old temp keys (older than 4 mins)
242
  for (const [key, value] of tempKeys.entries()) {
243
  if (now - value.createdAt > (1000 * 60 * 4)) {
244
  tempKeys.delete(key);
@@ -249,18 +217,14 @@ app.get('/cleanup', (req, res) => {
249
  res.json({ message: `Cleaned ${cleanedCount} cached sessions from memory.` });
250
  });
251
 
252
- // Endpoint: /nullify
253
- // Security Nuke: Removes session from Memory AND Firebase.
254
  app.post('/nullify', verifyFirebaseUser, async (req, res) => {
255
  const { projectId } = req.body;
256
  if (!projectId) return res.status(400).json({ error: 'projectId required' });
257
 
258
  const cacheKey = `${req.user.uid}:${projectId}`;
259
 
260
- // 1. Remove from Memory
261
  const existedInMemory = activeSessions.delete(cacheKey);
262
 
263
- // 2. Remove from Firebase
264
  if (db) {
265
  try {
266
  await db.ref(`plugin_oauth/${req.user.uid}/${projectId}`).remove();
@@ -297,107 +261,74 @@ app.get('/', (req, res) => {
297
  button.danger { background: #a51d2d; }
298
  label { display: block; margin-top: 10px; color: #9cdcfe; }
299
  pre { background: #000; padding: 10px; border: 1px solid #444; overflow-x: auto; white-space: pre-wrap; }
300
- .status { margin-top: 20px; padding: 10px; border-left: 4px solid #0e639c; background: #2d2d2d; }
301
  </style>
302
  </head>
303
  <body>
304
  <h1>🔌 Plugin Auth Debugger</h1>
305
-
306
  <div class="container">
307
  <h3>1. Configuration</h3>
308
- <label>Firebase ID Token (leave empty if DEBUG_NO_AUTH=true)</label>
309
  <input type="text" id="fbToken" placeholder="eyJhbG...">
310
-
311
  <label>Project ID</label>
312
  <input type="text" id="projId" value="roblox_world_1">
313
-
314
  <hr style="border-color:#444">
315
-
316
- <h3>2. Generate Key (/key)</h3>
317
  <button onclick="generateKey()">Generate PROJ_ Key</button>
318
- <pre id="keyResult">Waiting...</pre>
319
 
320
- <h3>3. Redeem Token (/redeem)</h3>
321
- <label>Key to Redeem</label>
322
  <input type="text" id="redeemKeyInput">
323
  <button onclick="redeemKey()">Redeem for JWT</button>
324
- <pre id="jwtResult">Waiting...</pre>
325
 
326
- <h3>4. Poll External (/poll)</h3>
327
- <label>JWT Token</label>
328
  <input type="text" id="jwtInput">
329
  <button onclick="poll()">Poll External Server</button>
330
- <pre id="pollResult">Waiting...</pre>
331
 
332
- <h3>5. Management</h3>
333
- <div style="display:flex; gap:10px;">
334
  <button onclick="cleanup()">Memory Cleanup</button>
335
  <button class="danger" onclick="nullify()">Nullify (Nuke)</button>
336
  </div>
337
- <pre id="mgmtResult">Waiting...</pre>
338
  </div>
339
-
340
  <script>
341
  const baseUrl = '';
342
-
343
  async function generateKey() {
344
  const token = document.getElementById('fbToken').value;
345
  const projectId = document.getElementById('projId').value;
346
-
347
  const headers = {};
348
  if(token) headers['Authorization'] = 'Bearer ' + token;
349
-
350
- const res = await fetch(baseUrl + '/key', {
351
- method: 'POST',
352
- headers: { 'Content-Type': 'application/json', ...headers },
353
- body: JSON.stringify({ projectId })
354
- });
355
  const data = await res.json();
356
  document.getElementById('keyResult').innerText = JSON.stringify(data, null, 2);
357
  if(data.key) document.getElementById('redeemKeyInput').value = data.key;
358
  }
359
-
360
  async function redeemKey() {
361
  const key = document.getElementById('redeemKeyInput').value;
362
- const res = await fetch(baseUrl + '/redeem', {
363
- method: 'POST',
364
- headers: { 'Content-Type': 'application/json' },
365
- body: JSON.stringify({ key })
366
- });
367
  const data = await res.json();
368
  document.getElementById('jwtResult').innerText = JSON.stringify(data, null, 2);
369
  if(data.token) document.getElementById('jwtInput').value = data.token;
370
  }
371
-
372
  async function poll() {
373
  const token = document.getElementById('jwtInput').value;
374
- const res = await fetch(baseUrl + '/poll', {
375
- method: 'POST',
376
- headers: { 'Content-Type': 'application/json' },
377
- body: JSON.stringify({ token, payload: { msg: "Hello from Roblox" } })
378
- });
379
  const data = await res.json();
380
  document.getElementById('pollResult').innerText = JSON.stringify(data, null, 2);
381
  }
382
-
383
  async function cleanup() {
384
- const res = await fetch(baseUrl + '/cleanup', { method: 'POST' });
385
  const data = await res.json();
386
  document.getElementById('mgmtResult').innerText = JSON.stringify(data, null, 2);
387
  }
388
-
389
  async function nullify() {
390
  const token = document.getElementById('fbToken').value;
391
  const projectId = document.getElementById('projId').value;
392
-
393
  const headers = {};
394
  if(token) headers['Authorization'] = 'Bearer ' + token;
395
-
396
- const res = await fetch(baseUrl + '/nullify', {
397
- method: 'POST',
398
- headers: { 'Content-Type': 'application/json', ...headers },
399
- body: JSON.stringify({ projectId })
400
- });
401
  const data = await res.json();
402
  document.getElementById('mgmtResult').innerText = JSON.stringify(data, null, 2);
403
  }
 
 
1
  const express = require('express');
2
  const admin = require('firebase-admin');
3
  const jwt = require('jsonwebtoken');
 
9
  app.use(bodyParser.json());
10
 
11
  // ---------------------------------------------------------
12
+ // 1. STATE MANAGEMENT
13
  // ---------------------------------------------------------
 
 
 
14
  const tempKeys = new Map();
 
 
 
15
  const activeSessions = new Map();
16
+ let db = null;
 
17
 
18
  // ---------------------------------------------------------
19
  // 2. FIREBASE INITIALIZATION
 
30
  db = admin.database();
31
  console.log("🔥 Firebase Connected & StateManager Linked");
32
  } else {
33
+ console.warn("⚠️ Memory-Only mode.");
34
  }
35
  } catch (e) {
36
  console.error("Firebase Init Error:", e);
37
  }
38
 
39
  // ---------------------------------------------------------
40
+ // 3. MIDDLEWARE
41
  // ---------------------------------------------------------
 
 
 
42
  const verifyFirebaseUser = async (req, res, next) => {
43
+ const debugMode = process.env.DEBUG_NO_AUTH === 'true';
44
 
45
  if (debugMode) {
46
  req.user = { uid: "debug_user_001" };
 
60
  req.user = decodedToken;
61
  next();
62
  } else {
 
63
  req.user = { uid: "memory_user" };
64
  next();
65
  }
 
68
  }
69
  };
70
 
 
71
  async function getSessionSecret(uid, projectId) {
72
  const cacheKey = `${uid}:${projectId}`;
 
 
73
  if (activeSessions.has(cacheKey)) {
74
  const session = activeSessions.get(cacheKey);
75
+ session.lastAccessed = Date.now();
76
  return session.secret;
77
  }
 
 
78
  if (db) {
79
  try {
80
  const snapshot = await db.ref(`plugin_oauth/${uid}/${projectId}`).once('value');
81
  if (snapshot.exists()) {
82
  const secret = snapshot.val();
 
83
  activeSessions.set(cacheKey, { secret, lastAccessed: Date.now() });
84
  console.log(`💧 Hydrated secret for ${cacheKey} from DB`);
85
  return secret;
 
88
  console.error("DB Read Error:", err);
89
  }
90
  }
 
 
91
  return null;
92
  }
93
 
 
95
  // 4. ENDPOINTS
96
  // ---------------------------------------------------------
97
 
 
 
98
  app.post('/key', verifyFirebaseUser, (req, res) => {
99
  const { projectId } = req.body;
100
  if (!projectId) return res.status(400).json({ error: 'projectId required' });
101
 
102
  const key = `proj_${uuidv4().replace(/-/g, '')}`;
103
 
 
104
  tempKeys.set(key, {
105
  uid: req.user.uid,
106
  projectId: projectId,
 
111
  res.json({ key, expiresIn: 300 });
112
  });
113
 
 
 
114
  app.post('/redeem', async (req, res) => {
115
  const { key } = req.body;
116
 
 
119
  }
120
 
121
  const data = tempKeys.get(key);
 
 
122
  const sessionSecret = uuidv4();
123
 
124
+ // UPDATED: Set expiresIn to '3d' (3 days)
125
  const token = jwt.sign(
126
  { uid: data.uid, projectId: data.projectId },
127
  sessionSecret,
128
+ { expiresIn: '3d' }
129
  );
130
 
131
  const cacheKey = `${data.uid}:${data.projectId}`;
132
 
 
133
  activeSessions.set(cacheKey, { secret: sessionSecret, lastAccessed: Date.now() });
134
 
 
135
  if (db) {
136
  await db.ref(`plugin_oauth/${data.uid}/${data.projectId}`).set(sessionSecret);
137
  }
138
 
 
139
  tempKeys.delete(key);
 
140
  console.log(`🚀 Redeemed JWT for ${cacheKey}`);
141
  res.json({ token });
142
  });
143
 
 
 
144
  app.post('/poll', async (req, res) => {
145
  const { token, payload: clientPayload } = req.body;
146
 
147
  if (!token) return res.status(400).json({ error: 'Token required' });
148
 
149
+ // Decode without verify first to find user/project ID
150
  const decoded = jwt.decode(token);
151
  if (!decoded || !decoded.uid || !decoded.projectId) {
152
  return res.status(401).json({ error: 'Malformed token' });
153
  }
154
 
 
155
  const secret = await getSessionSecret(decoded.uid, decoded.projectId);
156
 
157
  if (!secret) {
158
  return res.status(404).json({ error: 'Session revoked or not found' });
159
  }
160
 
 
161
  try {
162
+ // 1. Verify Signature and Expiration (checks exp claim)
163
+ const verifiedData = jwt.verify(token, secret);
164
+
165
+ // 2. EXPLICIT CHECK: Ensure token is not older than 3 days
166
+ // Even if expiresIn was 7d, this blocks it after 3d.
167
+ const threeDaysInSeconds = 3 * 24 * 60 * 60;
168
+ const nowInSeconds = Math.floor(Date.now() / 1000);
169
 
170
+ if (verifiedData.iat && (nowInSeconds - verifiedData.iat > threeDaysInSeconds)) {
171
+ console.log("⚠️ Token is valid signature, but too old.");
172
+ return res.status(403).json({ error: 'Token expired (older than 3 days)' });
173
+ }
174
 
175
+ const externalUrl = process.env.EXTERNAL_SERVER_URL || 'https://httpbin.org/post';
 
176
 
177
  try {
178
  const response = await axios.post(externalUrl, {
179
+ projectId: verifiedData.projectId,
180
+ // Add client data if needed
181
+ // data: clientPayload
182
  });
183
 
 
 
184
  return res.json({ status: 'success', externalResponse: response.data });
185
  } catch (extError) {
186
  return res.status(502).json({ error: 'External server error' });
187
  }
188
 
189
  } catch (err) {
190
+ if (err.name === 'TokenExpiredError') {
191
+ return res.status(403).json({ error: 'Token has expired' });
192
+ }
193
  return res.status(403).json({ error: 'Invalid Token Signature' });
194
  }
195
  });
196
 
197
+ // Changed to .all or .post because the HTML script sends a POST request
 
 
198
  app.get('/cleanup', (req, res) => {
199
  const THRESHOLD = 1000 * 60 * 60; // 1 Hour
200
  const now = Date.now();
 
207
  }
208
  }
209
 
 
210
  for (const [key, value] of tempKeys.entries()) {
211
  if (now - value.createdAt > (1000 * 60 * 4)) {
212
  tempKeys.delete(key);
 
217
  res.json({ message: `Cleaned ${cleanedCount} cached sessions from memory.` });
218
  });
219
 
 
 
220
  app.post('/nullify', verifyFirebaseUser, async (req, res) => {
221
  const { projectId } = req.body;
222
  if (!projectId) return res.status(400).json({ error: 'projectId required' });
223
 
224
  const cacheKey = `${req.user.uid}:${projectId}`;
225
 
 
226
  const existedInMemory = activeSessions.delete(cacheKey);
227
 
 
228
  if (db) {
229
  try {
230
  await db.ref(`plugin_oauth/${req.user.uid}/${projectId}`).remove();
 
261
  button.danger { background: #a51d2d; }
262
  label { display: block; margin-top: 10px; color: #9cdcfe; }
263
  pre { background: #000; padding: 10px; border: 1px solid #444; overflow-x: auto; white-space: pre-wrap; }
 
264
  </style>
265
  </head>
266
  <body>
267
  <h1>🔌 Plugin Auth Debugger</h1>
 
268
  <div class="container">
269
  <h3>1. Configuration</h3>
270
+ <label>Firebase ID Token</label>
271
  <input type="text" id="fbToken" placeholder="eyJhbG...">
 
272
  <label>Project ID</label>
273
  <input type="text" id="projId" value="roblox_world_1">
 
274
  <hr style="border-color:#444">
275
+
276
+ <h3>2. Actions</h3>
277
  <button onclick="generateKey()">Generate PROJ_ Key</button>
278
+ <pre id="keyResult">...</pre>
279
 
280
+ <label>Redeem Key</label>
 
281
  <input type="text" id="redeemKeyInput">
282
  <button onclick="redeemKey()">Redeem for JWT</button>
283
+ <pre id="jwtResult">...</pre>
284
 
285
+ <label>Poll (JWT)</label>
 
286
  <input type="text" id="jwtInput">
287
  <button onclick="poll()">Poll External Server</button>
288
+ <pre id="pollResult">...</pre>
289
 
290
+ <div style="display:flex; gap:10px; margin-top:10px;">
 
291
  <button onclick="cleanup()">Memory Cleanup</button>
292
  <button class="danger" onclick="nullify()">Nullify (Nuke)</button>
293
  </div>
294
+ <pre id="mgmtResult">...</pre>
295
  </div>
 
296
  <script>
297
  const baseUrl = '';
 
298
  async function generateKey() {
299
  const token = document.getElementById('fbToken').value;
300
  const projectId = document.getElementById('projId').value;
 
301
  const headers = {};
302
  if(token) headers['Authorization'] = 'Bearer ' + token;
303
+ const res = await fetch(baseUrl + '/key', { method: 'POST', headers: { 'Content-Type': 'application/json', ...headers }, body: JSON.stringify({ projectId }) });
 
 
 
 
 
304
  const data = await res.json();
305
  document.getElementById('keyResult').innerText = JSON.stringify(data, null, 2);
306
  if(data.key) document.getElementById('redeemKeyInput').value = data.key;
307
  }
 
308
  async function redeemKey() {
309
  const key = document.getElementById('redeemKeyInput').value;
310
+ const res = await fetch(baseUrl + '/redeem', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ key }) });
 
 
 
 
311
  const data = await res.json();
312
  document.getElementById('jwtResult').innerText = JSON.stringify(data, null, 2);
313
  if(data.token) document.getElementById('jwtInput').value = data.token;
314
  }
 
315
  async function poll() {
316
  const token = document.getElementById('jwtInput').value;
317
+ const res = await fetch(baseUrl + '/poll', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token, payload: { msg: "Hello" } }) });
 
 
 
 
318
  const data = await res.json();
319
  document.getElementById('pollResult').innerText = JSON.stringify(data, null, 2);
320
  }
 
321
  async function cleanup() {
322
+ const res = await fetch(baseUrl + '/cleanup', { method: 'GET' });
323
  const data = await res.json();
324
  document.getElementById('mgmtResult').innerText = JSON.stringify(data, null, 2);
325
  }
 
326
  async function nullify() {
327
  const token = document.getElementById('fbToken').value;
328
  const projectId = document.getElementById('projId').value;
 
329
  const headers = {};
330
  if(token) headers['Authorization'] = 'Bearer ' + token;
331
+ const res = await fetch(baseUrl + '/nullify', { method: 'POST', headers: { 'Content-Type': 'application/json', ...headers }, body: JSON.stringify({ projectId }) });
 
 
 
 
 
332
  const data = await res.json();
333
  document.getElementById('mgmtResult').innerText = JSON.stringify(data, null, 2);
334
  }