Add CORS and move registration policies to middleware
This commit is contained in:
@@ -0,0 +1,51 @@
|
|||||||
|
# Registration and Login Input Policy
|
||||||
|
|
||||||
|
This policy is for the website agent and describes the input rules enforced by
|
||||||
|
the API. The agent must validate inputs before submitting them and must never
|
||||||
|
silently change a user’s password.
|
||||||
|
|
||||||
|
## Registration
|
||||||
|
|
||||||
|
Registration accepts these required fields:
|
||||||
|
|
||||||
|
| Field | Rules |
|
||||||
|
| --- | --- |
|
||||||
|
| `name` | Required; 1–100 characters after trimming. |
|
||||||
|
| `username` | Required; 1–50 characters after trimming; must not contain whitespace. |
|
||||||
|
| `email` | Required; 1–254 characters after trimming; converted to lowercase; must not contain whitespace; must match a standard `name@domain.tld`-style format. |
|
||||||
|
| `password` | Required; 1–128 characters; whitespace is preserved. |
|
||||||
|
|
||||||
|
The registration password must also:
|
||||||
|
|
||||||
|
- Be at least 12 characters long.
|
||||||
|
- Contain at least one lowercase letter, one uppercase letter, one number, and one special character.
|
||||||
|
- Not contain the username or the email’s local part.
|
||||||
|
- Not be an obvious common password, including `password`, `password123`, `qwerty`, `letmein`, `welcome`, `admin`, `iloveyou`, `monkey`, `dragon`, or `abc123`.
|
||||||
|
- Not contain three identical characters in a row.
|
||||||
|
- Not contain obvious sequential patterns such as `1234`, `2345`, `6789`, or `abcd`.
|
||||||
|
- Not belong to a disposable or temporary email address.
|
||||||
|
|
||||||
|
## Login
|
||||||
|
|
||||||
|
Login accepts only `email` and `password`:
|
||||||
|
|
||||||
|
- Both fields are required and may contain 1–254 characters for email and 1–128 characters for password.
|
||||||
|
- Email is trimmed, converted to lowercase, and must not contain whitespace.
|
||||||
|
- Email must pass the same `name@domain.tld`-style format check used during registration.
|
||||||
|
- Password whitespace is preserved; the password must be submitted exactly as entered during registration.
|
||||||
|
- Registration-only password complexity checks and disposable-email checks are not repeated during login.
|
||||||
|
|
||||||
|
The agent must not trim, lowercase, or otherwise transform the password. The
|
||||||
|
API removes control characters from input values; ordinary password whitespace
|
||||||
|
is intentionally retained.
|
||||||
|
|
||||||
|
## Invalid input and authentication failures
|
||||||
|
|
||||||
|
- Do not submit a form until all applicable client-side rules pass.
|
||||||
|
- If the API returns HTTP `400`, show the validation message and let the user correct the input.
|
||||||
|
- If login returns HTTP `401`, show a generic “Invalid email or password” message. Do not reveal whether the email exists.
|
||||||
|
- Never log or expose passwords, password hashes, salts, or authentication tokens.
|
||||||
|
- Unexpected server failures must be shown as a generic failure message and retried only when appropriate; do not expose database or stack-trace details.
|
||||||
|
|
||||||
|
The API remains the authoritative validator. Client-side validation improves the
|
||||||
|
user experience but must not replace server-side validation.
|
||||||
@@ -1,7 +1,16 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
|
const cors = require('cors');
|
||||||
const app = express();
|
const app = express();
|
||||||
const dotenv = require('dotenv');
|
const dotenv = require('dotenv');
|
||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
|
|
||||||
|
const allowedOrigins = (process.env.CORS_ALLOWED_ORIGINS || '')
|
||||||
|
.split(',')
|
||||||
|
.map((origin) => origin.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
// Apply configured CORS origins to every endpoint, including preflight requests.
|
||||||
|
app.use(cors({ origin: allowedOrigins }));
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
// Data routes
|
// Data routes
|
||||||
@@ -14,9 +23,11 @@ app.use('/post_data', postDataRoute);
|
|||||||
app.use('/delete_data', deleteDataRoute);
|
app.use('/delete_data', deleteDataRoute);
|
||||||
|
|
||||||
// People routes
|
// People routes
|
||||||
const CreatePersonRoute = require('./features/people/CREATE_people');
|
const CreatePersonRoute = require('./features/people/REGISTER_people');
|
||||||
|
const LoginPersonRoute = require('./features/people/LOGIN_people');
|
||||||
|
|
||||||
app.use('/create_person', CreatePersonRoute);
|
app.use('/auth/register', CreatePersonRoute);
|
||||||
|
app.use('/auth/login', LoginPersonRoute);
|
||||||
|
|
||||||
// Relation routes
|
// Relation routes
|
||||||
const CreateRelationRoute = require('./features/relations/CREATE_friendship');
|
const CreateRelationRoute = require('./features/relations/CREATE_friendship');
|
||||||
@@ -26,4 +37,4 @@ app.use('/create_relationship', CreateRelationRoute)
|
|||||||
|
|
||||||
app.listen(process.env.PORT, () => {
|
app.listen(process.env.PORT, () => {
|
||||||
console.log(`Server draait op port ${process.env.PORT}`)
|
console.log(`Server draait op port ${process.env.PORT}`)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
const express = require('express');
|
|
||||||
const router = express.Router();
|
|
||||||
var driver = require('../../graph_db');
|
|
||||||
const pool = require('../../db');
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async function create_people(req, res) {
|
|
||||||
const { name, age, gender } = req.body;
|
|
||||||
try {
|
|
||||||
const { records } = await driver.executeQuery('Create (p:Person {name: $name, age: $age, gender: $gender})', { name: name, age: age, gender: gender }, { database: process.env.GRAPH_DB_NAME});
|
|
||||||
}
|
|
||||||
catch (err) {
|
|
||||||
console.error('database query failed', {
|
|
||||||
message: err.message,
|
|
||||||
code: err.code,
|
|
||||||
detail: err.detail,
|
|
||||||
hint: err.hint,
|
|
||||||
stack: err.stack,
|
|
||||||
});
|
|
||||||
res.status(500).send('Data was not pushed to graph database, request failed');
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const { rows } = await pool.query('INSERT INTO people (name, age, gender) VALUES ($1, $2, $3)', [name, age, gender]);
|
|
||||||
console.log(rows[0]);
|
|
||||||
res.status(200).send('Data has been pushed to both databases');
|
|
||||||
res.json(rows[0]);
|
|
||||||
}
|
|
||||||
catch (err) {
|
|
||||||
console.error('database query failed', {
|
|
||||||
message: err.message,
|
|
||||||
code: err.code,
|
|
||||||
detail: err.detail,
|
|
||||||
hint: err.hint,
|
|
||||||
stack: err.stack,
|
|
||||||
});
|
|
||||||
res.status(500).send('Data was not pushed to database, request failed');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
router.post('/', create_people);
|
|
||||||
module.exports = router;
|
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const pool = require('../../db');
|
||||||
|
const crypto = require('crypto');
|
||||||
|
const { promisify } = require('util');
|
||||||
|
const { sanitizeLoginInput } = require('../../middleware/sanitize_person_input');
|
||||||
|
|
||||||
|
const scrypt = promisify(crypto.scrypt);
|
||||||
|
|
||||||
|
async function login(req, res) {
|
||||||
|
try {
|
||||||
|
const { email, password } = req.loginInput;
|
||||||
|
const { rows } = await pool.query(
|
||||||
|
'SELECT id, password FROM people WHERE email = $1',
|
||||||
|
[email]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (rows.length === 0) {
|
||||||
|
res.status(401).send('Invalid email or password');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [salt, storedKeyHex] = rows[0].password.split(':');
|
||||||
|
const storedKey = Buffer.from(storedKeyHex || '', 'hex');
|
||||||
|
const derivedKey = await scrypt(password, salt, 64);
|
||||||
|
|
||||||
|
if (
|
||||||
|
storedKey.length !== derivedKey.length ||
|
||||||
|
!crypto.timingSafeEqual(storedKey, derivedKey)
|
||||||
|
) {
|
||||||
|
res.status(401).send('Invalid email or password');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({ id: rows[0].id });
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error('login failed', { message: err.message, code: err.code });
|
||||||
|
res.status(500).send('Login failed, request could not be completed');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
router.post('/', sanitizeLoginInput, login);
|
||||||
|
module.exports = router;
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
var driver = require('../../graph_db');
|
||||||
|
const pool = require('../../db');
|
||||||
|
var crypto = require('crypto');
|
||||||
|
const { promisify } = require('util');
|
||||||
|
const { sanitizePersonInput } = require('../../middleware/sanitize_person_input');
|
||||||
|
|
||||||
|
const scrypt = promisify(crypto.scrypt);
|
||||||
|
|
||||||
|
async function insert_db( name, username, email, password ) {
|
||||||
|
const salt = crypto.randomBytes(16).toString('hex');
|
||||||
|
const derivedKey = await scrypt(password, salt, 64);
|
||||||
|
const hashed_password = `${salt}:${derivedKey.toString('hex')}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { rows } = await pool.query(
|
||||||
|
'INSERT INTO people (name, username, email, password) VALUES ($1, $2, $3, $4) RETURNING id',
|
||||||
|
[name, username, email, hashed_password]
|
||||||
|
);
|
||||||
|
|
||||||
|
return rows[0].id;
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error('database query failed', {
|
||||||
|
message: err.message,
|
||||||
|
code: err.code,
|
||||||
|
detail: err.detail,
|
||||||
|
hint: err.hint,
|
||||||
|
stack: err.stack,
|
||||||
|
});
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async function insert_graph_db( name, username, db_id ) {
|
||||||
|
try {
|
||||||
|
await driver.executeQuery(
|
||||||
|
'CREATE (p:Person {db_id: $db_id, name: $name, username: $username})',
|
||||||
|
{ db_id, name, username },
|
||||||
|
{ database: process.env.GRAPH_DB_NAME }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error('database query failed', {
|
||||||
|
message: err.message,
|
||||||
|
code: err.code,
|
||||||
|
detail: err.detail,
|
||||||
|
hint: err.hint,
|
||||||
|
stack: err.stack,
|
||||||
|
});
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async function create_people(req, res) {
|
||||||
|
const person = req.person;
|
||||||
|
let db_id;
|
||||||
|
|
||||||
|
try {
|
||||||
|
db_id = await insert_db(
|
||||||
|
person.name,
|
||||||
|
person.username,
|
||||||
|
person.email,
|
||||||
|
person.password
|
||||||
|
);
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
res.status(500).send('Data was not pushed to database, request failed');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await insert_graph_db(person.name, person.username, db_id);
|
||||||
|
res.status(201).json({ id: db_id });
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
res.status(500).send('Data was not pushed to graph database, request failed');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
router.post('/', sanitizePersonInput, create_people);
|
||||||
|
module.exports = router;
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
const { isDisposableEmail } = require('fakeout');
|
||||||
|
|
||||||
|
const LIMITS = {
|
||||||
|
name: 100,
|
||||||
|
username: 50,
|
||||||
|
email: 254,
|
||||||
|
password: 128,
|
||||||
|
};
|
||||||
|
|
||||||
|
const MIN_PASSWORD_LENGTH = 12;
|
||||||
|
const COMMON_PASSWORDS = new Set([
|
||||||
|
'123456789', 'password', 'password123', 'qwerty',
|
||||||
|
'qwertyuiop', 'letmein', 'welcome', 'admin',
|
||||||
|
'iloveyou', 'monkey', 'dragon', 'abc123',
|
||||||
|
]);
|
||||||
|
|
||||||
|
function cleanText(value, { trim = true } = {}) {
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleaned = value.replace(/[\u0000-\u001F\u007F]/g, '');
|
||||||
|
return trim ? cleaned.trim() : cleaned;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanPersonInput(input) {
|
||||||
|
return {
|
||||||
|
name: cleanText(input.name),
|
||||||
|
username: cleanText(input.username),
|
||||||
|
email: cleanText(input.email).toLowerCase(),
|
||||||
|
// Password whitespace is preserved, but control characters are removed.
|
||||||
|
password: cleanText(input.password, { trim: false }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateCommonFields(values, { requireName = true } = {}) {
|
||||||
|
const fields = requireName
|
||||||
|
? ['name', 'username', 'email', 'password']
|
||||||
|
: ['email', 'password'];
|
||||||
|
|
||||||
|
for (const field of fields) {
|
||||||
|
if (!values[field] || values[field].length > LIMITS[field]) {
|
||||||
|
throw new Error(`${field} must be between 1 and ${LIMITS[field]} characters`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/\s/.test(values.username)) {
|
||||||
|
throw new Error('username must not contain spaces');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/\s/.test(values.email)) {
|
||||||
|
throw new Error('email must not contain spaces');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(values.email)) {
|
||||||
|
throw new Error('email must be a valid email address');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateRegistrationInput(values) {
|
||||||
|
validateCommonFields(values);
|
||||||
|
|
||||||
|
if (values.password.length < MIN_PASSWORD_LENGTH) {
|
||||||
|
throw new Error(`password must be at least ${MIN_PASSWORD_LENGTH} characters`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const passwordLower = values.password.toLowerCase();
|
||||||
|
const normalizedPassword = passwordLower.replace(/[^a-z0-9]/g, '');
|
||||||
|
const emailLocalPart = values.email.split('@')[0];
|
||||||
|
if (
|
||||||
|
COMMON_PASSWORDS.has(normalizedPassword) ||
|
||||||
|
passwordLower.includes(values.username.toLowerCase()) ||
|
||||||
|
passwordLower.includes(emailLocalPart)
|
||||||
|
) {
|
||||||
|
throw new Error('password is too common or contains account details');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!/[a-z]/.test(values.password) || !/[A-Z]/.test(values.password)) {
|
||||||
|
throw new Error('password must contain lowercase and uppercase letters');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!/[0-9]/.test(values.password)) {
|
||||||
|
throw new Error('password must contain a number');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!/[^a-zA-Z0-9\s]/.test(values.password)) {
|
||||||
|
throw new Error('password must contain a special character');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/(.)\1\1/.test(values.password) || /(0123|1234|2345|3456|4567|5678|6789|abcd|bcde|cdef)/i.test(values.password)) {
|
||||||
|
throw new Error('password must not contain obvious repeated or sequential characters');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDisposableEmail(values.email)) {
|
||||||
|
throw new Error('temporary email addresses are not allowed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizePersonInput(req, res, next) {
|
||||||
|
try {
|
||||||
|
const values = cleanPersonInput(req.body || {});
|
||||||
|
validateRegistrationInput(values);
|
||||||
|
req.person = values;
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
res.status(400).send(err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeLoginInput(req, res, next) {
|
||||||
|
try {
|
||||||
|
const values = cleanPersonInput(req.body || {});
|
||||||
|
validateCommonFields(values, { requireName: false });
|
||||||
|
req.loginInput = { email: values.email, password: values.password };
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
res.status(400).send(err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { sanitizePersonInput, sanitizeLoginInput };
|
||||||
Generated
+37
@@ -9,8 +9,10 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"cors": "^2.8.6",
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
|
"fakeout": "^1.0.65",
|
||||||
"neo4j-driver": "^6.2.0",
|
"neo4j-driver": "^6.2.0",
|
||||||
"pg": "^8.22.0",
|
"pg": "^8.22.0",
|
||||||
"router": "^2.2.0"
|
"router": "^2.2.0"
|
||||||
@@ -279,6 +281,23 @@
|
|||||||
"node": ">=6.6.0"
|
"node": ">=6.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cors": {
|
||||||
|
"version": "2.8.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
|
||||||
|
"integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"object-assign": "^4",
|
||||||
|
"vary": "^1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
@@ -434,6 +453,15 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fakeout": {
|
||||||
|
"version": "1.0.65",
|
||||||
|
"resolved": "https://registry.npmjs.org/fakeout/-/fakeout-1.0.65.tgz",
|
||||||
|
"integrity": "sha512-VaHtvOaBuSpetcCU243ex4bMII2SLiTq9FS6NyD23k0doFRIM0v3+a8FtrMlQf9s8XsXkMUClt6Oc5N9V73rSQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fill-range": {
|
"node_modules/fill-range": {
|
||||||
"version": "7.1.1",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||||
@@ -892,6 +920,15 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/object-assign": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
|
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/object-inspect": {
|
"node_modules/object-inspect": {
|
||||||
"version": "1.13.4",
|
"version": "1.13.4",
|
||||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||||
|
|||||||
@@ -12,8 +12,10 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"cors": "^2.8.6",
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
|
"fakeout": "^1.0.65",
|
||||||
"neo4j-driver": "^6.2.0",
|
"neo4j-driver": "^6.2.0",
|
||||||
"pg": "^8.22.0",
|
"pg": "^8.22.0",
|
||||||
"router": "^2.2.0"
|
"router": "^2.2.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user