first commit
This commit is contained in:
163
api/index.js
Normal file
163
api/index.js
Normal file
@@ -0,0 +1,163 @@
|
||||
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}`);
|
||||
});
|
||||
23
api/package.json
Normal file
23
api/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "secrets-manager-api",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "node index.js",
|
||||
"dev": "nodemon index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"pg": "^8.11.3",
|
||||
"redis": "^4.6.10",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"secure-remote-password": "^3.0.0",
|
||||
"morgan": "^1.10.0",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.1"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user