File size: 14,838 Bytes
c0b7bc4
c78bf1e
 
 
 
 
 
9903c38
ab9b3de
c78bf1e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b6fcea9
c78bf1e
 
 
3456cb8
c78bf1e
 
 
 
ab9b3de
c78bf1e
 
 
 
8edba50
c78bf1e
 
 
 
8edba50
c78bf1e
c9f2107
b6fcea9
c78bf1e
 
 
 
 
 
 
 
 
 
 
 
 
c9f2107
c78bf1e
 
 
b6fcea9
c78bf1e
 
 
 
 
 
b6fcea9
c78bf1e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b6fcea9
c78bf1e
 
 
b6fcea9
c78bf1e
 
 
c9f2107
c78bf1e
 
 
 
 
0787978
c78bf1e
 
 
 
 
 
 
 
8edba50
c78bf1e
 
 
b6fcea9
c78bf1e
 
 
 
 
 
 
b6fcea9
 
c78bf1e
712d3b7
c78bf1e
 
 
 
 
 
 
 
 
c9f2107
c78bf1e
8edba50
c78bf1e
 
b6fcea9
c78bf1e
 
 
 
b6fcea9
c78bf1e
 
b6fcea9
c78bf1e
 
 
0787978
c78bf1e
 
 
 
0787978
c78bf1e
8edba50
c78bf1e
 
 
 
 
b6fcea9
c78bf1e
 
b6fcea9
c78bf1e
 
b6fcea9
0787978
c78bf1e
b6fcea9
c78bf1e
8edba50
c78bf1e
 
b6fcea9
c78bf1e
 
 
 
 
 
 
 
 
 
c9f2107
0787978
c78bf1e
0787978
c9f2107
b6fcea9
c78bf1e
 
 
 
 
 
 
 
 
 
 
 
 
b6fcea9
c9f2107
c78bf1e
 
 
 
 
 
c9f2107
c78bf1e
 
8edba50
 
c78bf1e
 
 
8edba50
c78bf1e
c9f2107
c78bf1e
 
 
 
 
 
 
 
 
 
 
b6fcea9
9b3839b
c78bf1e
 
 
 
 
 
 
c9f2107
 
c78bf1e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8edba50
c78bf1e
8edba50
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405

const express = require('express');
const admin = require('firebase-admin');
const jwt = require('jsonwebtoken');
const { v4: uuidv4 } = require('uuid');
const axios = require('axios');
const bodyParser = require('body-parser');

const app = express();
app.use(bodyParser.json());

// ---------------------------------------------------------
// 1. STATE MANAGEMENT (In-Memory DB)
// ---------------------------------------------------------

// Store temporary "proj_" redemption keys.
// Structure: { "proj_xyz": { uid: "...", projectId: "...", expiresAt: timestamp } }
const tempKeys = new Map();

// Store JWT Secrets (Hydrated Cache).
// Structure: { "uid:projectId": { secret: "...", lastAccessed: timestamp } }
const activeSessions = new Map();

let db = null; // Firebase DB Reference

// ---------------------------------------------------------
// 2. FIREBASE INITIALIZATION
// ---------------------------------------------------------
try {
    if (process.env.FIREBASE_SERVICE_ACCOUNT_JSON) {
        const serviceAccount = JSON.parse(process.env.FIREBASE_SERVICE_ACCOUNT_JSON);
        if (admin.apps.length === 0) {
            admin.initializeApp({
                credential: admin.credential.cert(serviceAccount),
                databaseURL: process.env.FIREBASE_DB_URL || "https://your-firebase-project.firebaseio.com"
            });
        }
        db = admin.database();
        console.log("🔥 Firebase Connected & StateManager Linked");
    } else {
        console.warn("⚠️ Memory-Only mode (No Firebase JSON provided).");
    }
} catch (e) {
    console.error("Firebase Init Error:", e);
}

// ---------------------------------------------------------
// 3. HELPER FUNCTIONS & MIDDLEWARE
// ---------------------------------------------------------

// Middleware to verify Firebase ID Token (for /key and /nullify)
// Includes a Debug Bypass variable
const verifyFirebaseUser = async (req, res, next) => {
    const debugMode = process.env.DEBUG_NO_AUTH === 'true'; // Set to true in .env for testing without valid tokens

    if (debugMode) {
        req.user = { uid: "debug_user_001" };
        return next();
    }

    const authHeader = req.headers.authorization;
    if (!authHeader || !authHeader.startsWith('Bearer ')) {
        return res.status(401).json({ error: 'Missing Bearer token' });
    }

    const idToken = authHeader.split('Bearer ')[1];

    try {
        if (admin.apps.length > 0) {
            const decodedToken = await admin.auth().verifyIdToken(idToken);
            req.user = decodedToken;
            next();
        } else {
            // Fallback for memory-only mode without validation
            req.user = { uid: "memory_user" };
            next();
        }
    } catch (error) {
        return res.status(403).json({ error: 'Unauthorized', details: error.message });
    }
};

// Helper to fetch Secret (Memory -> DB -> 404)
async function getSessionSecret(uid, projectId) {
    const cacheKey = `${uid}:${projectId}`;

    // 1. Check Memory
    if (activeSessions.has(cacheKey)) {
        const session = activeSessions.get(cacheKey);
        session.lastAccessed = Date.now(); // Update access time for cleanup
        return session.secret;
    }

    // 2. Hydrate from DB
    if (db) {
        try {
            const snapshot = await db.ref(`plugin_oauth/${uid}/${projectId}`).once('value');
            if (snapshot.exists()) {
                const secret = snapshot.val();
                // Store in memory for next time
                activeSessions.set(cacheKey, { secret, lastAccessed: Date.now() });
                console.log(`💧 Hydrated secret for ${cacheKey} from DB`);
                return secret;
            }
        } catch (err) {
            console.error("DB Read Error:", err);
        }
    }

    // 3. Not found
    return null;
}

// ---------------------------------------------------------
// 4. ENDPOINTS
// ---------------------------------------------------------

// Endpoint: /key
// Generates a temporary 'proj_' token. Expects Firebase Auth.
app.post('/key', verifyFirebaseUser, (req, res) => {
    const { projectId } = req.body;
    if (!projectId) return res.status(400).json({ error: 'projectId required' });

    const key = `proj_${uuidv4().replace(/-/g, '')}`;
    
    // Store in memory (valid for 5 minutes)
    tempKeys.set(key, {
        uid: req.user.uid,
        projectId: projectId,
        createdAt: Date.now()
    });

    console.log(`🔑 Generated Key for user ${req.user.uid}: ${key}`);
    res.json({ key, expiresIn: 300 });
});

// Endpoint: /redeem
// Exchanges 'proj_' key for a JWT.
app.post('/redeem', async (req, res) => {
    const { key } = req.body;
    
    if (!key || !tempKeys.has(key)) {
        return res.status(404).json({ error: 'Invalid or expired key' });
    }

    const data = tempKeys.get(key);
    
    // Generate a unique secret for this session/project
    const sessionSecret = uuidv4(); 
    
    // Create JWT
    const token = jwt.sign(
        { uid: data.uid, projectId: data.projectId }, 
        sessionSecret, 
        { expiresIn: '7d' } // Plugin token validity
    );

    const cacheKey = `${data.uid}:${data.projectId}`;

    // 1. Store in Memory
    activeSessions.set(cacheKey, { secret: sessionSecret, lastAccessed: Date.now() });

    // 2. Store in Firebase (Persist)
    if (db) {
        await db.ref(`plugin_oauth/${data.uid}/${data.projectId}`).set(sessionSecret);
    }

    // Burn the redemption key (One-time use)
    tempKeys.delete(key);

    console.log(`🚀 Redeemed JWT for ${cacheKey}`);
    res.json({ token });
});

// Endpoint: /poll
// Verifies JWT using the stored secret and forwards to external server.
app.post('/poll', async (req, res) => {
    const { token, payload: clientPayload } = req.body;

    if (!token) return res.status(400).json({ error: 'Token required' });

    // Decode without verification first to find WHO it is
    const decoded = jwt.decode(token);
    if (!decoded || !decoded.uid || !decoded.projectId) {
        return res.status(401).json({ error: 'Malformed token' });
    }

    // Fetch secret (Memory -> DB -> 404)
    const secret = await getSessionSecret(decoded.uid, decoded.projectId);

    if (!secret) {
        return res.status(404).json({ error: 'Session revoked or not found' });
    }

    // Verify signature
    try {
        jwt.verify(token, secret);
        
        // If valid, post to external server
        const externalUrl = process.env.EXTERNAL_SERVER_URL || 'https://httpbin.org/post'; // Default for testing
        
        try {
            const response = await axios.post(externalUrl, {
                user: decoded.uid,
                project: decoded.projectId,
                data: clientPayload
            });
            return res.json({ status: 'success', externalResponse: response.data });
        } catch (extError) {
            return res.status(502).json({ error: 'External server error' });
        }

    } catch (err) {
        return res.status(403).json({ error: 'Invalid Token Signature' });
    }
});

// Endpoint: /cleanup
// Memory management: Clears old JWT secrets from RAM that haven't been used recently.
// Does NOT delete from Firebase.
app.post('/cleanup', (req, res) => {
    const THRESHOLD = 1000 * 60 * 60; // 1 Hour
    const now = Date.now();
    let cleanedCount = 0;

    for (const [key, value] of activeSessions.entries()) {
        if (now - value.lastAccessed > THRESHOLD) {
            activeSessions.delete(key);
            cleanedCount++;
        }
    }

    // Also clean old temp keys (older than 10 mins)
    for (const [key, value] of tempKeys.entries()) {
        if (now - value.createdAt > (1000 * 60 * 10)) {
            tempKeys.delete(key);
        }
    }

    console.log(`🧹 Garbage Collection: Removed ${cleanedCount} cached sessions.`);
    res.json({ message: `Cleaned ${cleanedCount} cached sessions from memory.` });
});

// Endpoint: /nullify
// Security Nuke: Removes session from Memory AND Firebase.
app.post('/nullify', verifyFirebaseUser, async (req, res) => {
    const { projectId } = req.body;
    if (!projectId) return res.status(400).json({ error: 'projectId required' });

    const cacheKey = `${req.user.uid}:${projectId}`;

    // 1. Remove from Memory
    const existedInMemory = activeSessions.delete(cacheKey);

    // 2. Remove from Firebase
    if (db) {
        try {
            await db.ref(`plugin_oauth/${req.user.uid}/${projectId}`).remove();
        } catch (e) {
            return res.status(500).json({ error: 'Database error during nullify' });
        }
    }

    console.log(`☢️ NULLIFIED session for ${cacheKey}`);
    res.json({ 
        success: true, 
        message: 'Session secrets purged from memory and database.',
        wasCached: existedInMemory 
    });
});

// ---------------------------------------------------------
// 5. EMBEDDED HTML DEBUGGER
// ---------------------------------------------------------
app.get('/', (req, res) => {
    res.send(`
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Roblox Plugin Auth Server</title>
        <style>
            body { font-family: monospace; background: #1e1e1e; color: #d4d4d4; padding: 20px; max-width: 800px; margin: 0 auto; }
            .container { background: #252526; padding: 20px; border-radius: 8px; border: 1px solid #333; }
            input, button { padding: 10px; margin: 5px 0; width: 100%; box-sizing: border-box; background: #3c3c3c; border: 1px solid #555; color: white; }
            button { background: #0e639c; cursor: pointer; font-weight: bold; }
            button:hover { background: #1177bb; }
            button.danger { background: #a51d2d; }
            label { display: block; margin-top: 10px; color: #9cdcfe; }
            pre { background: #000; padding: 10px; border: 1px solid #444; overflow-x: auto; white-space: pre-wrap; }
            .status { margin-top: 20px; padding: 10px; border-left: 4px solid #0e639c; background: #2d2d2d; }
        </style>
    </head>
    <body>
        <h1>🔌 Plugin Auth Debugger</h1>
        
        <div class="container">
            <h3>1. Configuration</h3>
            <label>Firebase ID Token (leave empty if DEBUG_NO_AUTH=true)</label>
            <input type="text" id="fbToken" placeholder="eyJhbG...">
            
            <label>Project ID</label>
            <input type="text" id="projId" value="roblox_world_1">

            <hr style="border-color:#444">

            <h3>2. Generate Key (/key)</h3>
            <button onclick="generateKey()">Generate PROJ_ Key</button>
            <pre id="keyResult">Waiting...</pre>

            <h3>3. Redeem Token (/redeem)</h3>
            <label>Key to Redeem</label>
            <input type="text" id="redeemKeyInput">
            <button onclick="redeemKey()">Redeem for JWT</button>
            <pre id="jwtResult">Waiting...</pre>

            <h3>4. Poll External (/poll)</h3>
            <label>JWT Token</label>
            <input type="text" id="jwtInput">
            <button onclick="poll()">Poll External Server</button>
            <pre id="pollResult">Waiting...</pre>

            <h3>5. Management</h3>
            <div style="display:flex; gap:10px;">
                <button onclick="cleanup()">Memory Cleanup</button>
                <button class="danger" onclick="nullify()">Nullify (Nuke)</button>
            </div>
            <pre id="mgmtResult">Waiting...</pre>
        </div>

        <script>
            const baseUrl = '';

            async function generateKey() {
                const token = document.getElementById('fbToken').value;
                const projectId = document.getElementById('projId').value;
                
                const headers = {};
                if(token) headers['Authorization'] = 'Bearer ' + token;

                const res = await fetch(baseUrl + '/key', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json', ...headers },
                    body: JSON.stringify({ projectId })
                });
                const data = await res.json();
                document.getElementById('keyResult').innerText = JSON.stringify(data, null, 2);
                if(data.key) document.getElementById('redeemKeyInput').value = data.key;
            }

            async function redeemKey() {
                const key = document.getElementById('redeemKeyInput').value;
                const res = await fetch(baseUrl + '/redeem', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ key })
                });
                const data = await res.json();
                document.getElementById('jwtResult').innerText = JSON.stringify(data, null, 2);
                if(data.token) document.getElementById('jwtInput').value = data.token;
            }

            async function poll() {
                const token = document.getElementById('jwtInput').value;
                const res = await fetch(baseUrl + '/poll', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ token, payload: { msg: "Hello from Roblox" } })
                });
                const data = await res.json();
                document.getElementById('pollResult').innerText = JSON.stringify(data, null, 2);
            }

            async function cleanup() {
                const res = await fetch(baseUrl + '/cleanup', { method: 'POST' });
                const data = await res.json();
                document.getElementById('mgmtResult').innerText = JSON.stringify(data, null, 2);
            }

            async function nullify() {
                const token = document.getElementById('fbToken').value;
                const projectId = document.getElementById('projId').value;
                
                const headers = {};
                if(token) headers['Authorization'] = 'Bearer ' + token;

                const res = await fetch(baseUrl + '/nullify', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json', ...headers },
                    body: JSON.stringify({ projectId })
                });
                const data = await res.json();
                document.getElementById('mgmtResult').innerText = JSON.stringify(data, null, 2);
            }
        </script>
    </body>
    </html>
    `);
});

const PORT = process.env.PORT || 7860;
app.listen(PORT, () => {
    console.log(`🚀 Server running on http://localhost:${PORT}`);
});