const express = require('express'); const cors = require('cors'); const morgan = require('morgan'); const { Pool } = require('pg'); const { createClient } = require('redis'); const dotenv = require('dotenv'); dotenv.config(); const app = express(); const port = process.env.PORT || 3000; // Middleware app.use(cors()); app.use(express.json()); app.use(morgan('combined')); // Database const pool = new Pool({ connectionString: process.env.DATABASE_URL }); // Redis const redisClient = createClient({ url: process.env.REDIS_URL }); redisClient.on('error', (err) => console.log('Redis Client Error', err)); (async () => { await redisClient.connect(); console.log('Connected to Redis'); })(); // Health Check app.get('/health', (req, res) => { res.json({ status: 'ok', timestamp: new Date() }); }); const srp = require('secure-remote-password/server'); const jwt = require('jsonwebtoken'); // Route: Registration app.post('/auth/register', async (req, res) => { try { const { email, salt, verifier } = req.body; if (!email || !salt || !verifier) { return res.status(400).json({ error: 'Missing fields' }); } // Check if user exists const userCheck = await pool.query('SELECT id FROM api.users WHERE email = $1', [email]); if (userCheck.rows.length > 0) { return res.status(409).json({ error: 'User already exists' }); } await pool.query( 'INSERT INTO api.users (email, salt, verifier) VALUES ($1, $2, $3)', [email, salt, verifier] ); res.status(201).json({ message: 'User registered' }); } catch (err) { console.error(err); res.status(500).json({ error: 'Registration failed' }); } }); // Route: Login Step 1 app.post('/auth/login/step1', async (req, res) => { try { const { email, clientPublic } = req.body; // clientPublic is 'A' if (!email || !clientPublic) return res.status(400).json({ error: 'Missing email or clientPublic' }); const result = await pool.query('SELECT id, salt, verifier FROM api.users WHERE email = $1', [email]); if (result.rows.length === 0) { return res.status(404).json({ error: 'User not found' }); } const user = result.rows[0]; // Generate server ephemeral (B) const serverEphemeral = srp.generateEphemeral(user.verifier); // serverEphemeral contains { secret, public } (b, B) // Store session in Redis const sessionKey = `srp:${email}`; await redisClient.set(sessionKey, JSON.stringify({ salt: user.salt, verifier: user.verifier, serverSecret: serverEphemeral.secret, serverPublic: serverEphemeral.public, clientPublic: clientPublic, userId: user.id }), { EX: 300 // 5 minutes expiration }); res.json({ salt: user.salt, serverPublic: serverEphemeral.public }); } catch (err) { console.error(err); res.status(500).json({ error: 'Login step 1 failed' }); } }); // Route: Login Step 2 app.post('/auth/login/step2', async (req, res) => { try { const { email, clientProof } = req.body; // clientProof is 'M1' if (!email || !clientProof) return res.status(400).json({ error: 'Missing email or clientProof' }); const sessionKey = `srp:${email}`; const sessionDataString = await redisClient.get(sessionKey); if (!sessionDataString) { return res.status(401).json({ error: 'Session expired or invalid' }); } const session = JSON.parse(sessionDataString); // Calculate server session const serverSession = srp.deriveSession( session.serverSecret, session.clientPublic, session.salt, email, session.verifier, clientProof ); if (!serverSession) { return res.status(401).json({ error: 'Authentication failed' }); } // Generate JWT const token = jwt.sign( { sub: session.userId, role: 'authenticated', // For PostgREST email: email }, process.env.JWT_SECRET, { expiresIn: '1h' } ); // Clear session (optional, prevents replay of step 2 within window, though M1 is unique per session usually) await redisClient.del(sessionKey); res.json({ serverProof: serverSession.proof, // M2 token: token }); } catch (err) { console.error(err); res.status(500).json({ error: 'Login step 2 failed' }); } }); app.listen(port, () => { console.log(`API listening on port ${port}`); });