import { Request, Response } from 'express'; import { decode, sign, verify } from 'jsonwebtoken'; import { JWT_SECRET, logger, RABBITMQ_URI } from '../app'; import * as jwt from 'jsonwebtoken'; import { config } from '../config'; import { hashPassword, randomPepper, randomString, verifyPassword } from '../lib/crypto'; import { LivebeatRequest } from '../lib/request'; import { UserType } from '../models/user/user.interface'; import { User } from '../models/user/user.model'; export async function GetUser(req: LivebeatRequest, res: Response) { let user: any = req.params.id === undefined ? req.user : await User.findById(req.params.id); if (user === null) { res.status(404).send(); return; } user.password = undefined; user.salt = undefined; user.__v = undefined; res.status(200).send(user); } export async function PostUser(req: LivebeatRequest, res: Response) { // Only admin can create new users if (req.user?.type !== UserType.ADMIN) { res.status(401).send({ message: 'Only admins can create new users.' }); return; } const name = req.body.name; const password = req.body.password; const type = req.body.type; if (name === undefined || password === undefined || type === undefined) { res.status(400).send(); return; } if (!Object.values(UserType).includes(type)) { res.status(400).send({ message: 'The user type can only be \'guest\', \'user\', \'admin\'.' }); return; } if (await User.countDocuments({ name }) === 1) { res.status(409).send(); return; } const salt = randomString(config.authentification.salt_length); const brokerToken = randomString(16); const hashedPassword = await hashPassword(password + salt + randomPepper()).catch(error => { res.status(400).send({ message: 'Provided password is too weak and cannot be used.' }); return; }) as string; const newUser = await User.create({ name, password: hashedPassword, salt, brokerToken, type, lastLogin: new Date(0) }); // Create setup token that the new user can use to change his password. const setupToken = jwt.sign({ setupForUser: newUser._id }, JWT_SECRET, { expiresIn: '1d' }); res.status(200).send({ setupToken }); } export async function DeleteUser(req: Request, res: Response) { } export async function PatchUser(req: Request, res: Response) { } export async function LoginUser(req: Request, res: Response) { const username = req.body.username; const password = req.body.password; const twoFA = req.body.twoFA; const user = await User.findOne({ name: username }); // Check if user exists if (user == undefined) { setTimeout(() => { res.status(404).send({ message: "Either the username or password is wrong." }); }, Math.random() * 1500 + 400); return; } // Check if 2FA is turned on (the attack doesn't know yet if the password is wrong) if (user.twoFASecret != undefined) { if (twoFA == undefined) { res.status(401).send({ message: "2FA code is required." }); return; } // TODO: Implement 2FA logic here } // Check if password is wrong if (!await verifyPassword(password + user.salt, user.password)) { res.status(404).send({ message: 'Either the username or password is wrong.' }); return; } // We're good. Create JWT token. const token = sign({ user: user._id }, JWT_SECRET, { expiresIn: '30d' }); user.lastLogin = new Date(Date.now()); await user.save(); logger.info(`User ${user.name} logged in.`) res.status(200).send({ token }); } /** * This function handles all logins to RabbitMQ since they need a differnt type of response * then requests from frontends (web and phone). */ export async function LoginRabbitUser(req: Request, res: Response) { const username = req.query.username; const password = req.query.password; res.status(200); if (username === undefined || password === undefined) { res.send('deny'); return; } // Check if request comes from backend. Basicly, we permitting ourself to connect with RabbitMQ. if (username === "backend" && password === RABBITMQ_URI.split(':')[2].split('@')[0]) { res.send('allow administrator'); return; } // Get user from database const user = await User.findOne({ name: username.toString() }); // If we are here, it means we have a non-admin user. if (user === null) { res.send('deny'); return; } // Auth token for message broker is stored in plain text since it's randomly generated and only grants access to the broker. if (user.brokerToken === password.toString()) { if (user.type === UserType.ADMIN) { res.send('allow administrator'); } else { // Not an admin, grant user privilieges res.send('allow user') } return; } res.send('deny'); } /** * This function basicly allows access to the root vhost if the user is known. */ export async function VHost(req: Request, res: Response) { const vhost = req.query.vhost; const username = req.query.username; if (vhost === undefined || username === undefined) { res.status(200).send('deny'); return; } if (vhost != '/') { res.status(200).send('deny'); return; } // Check if user is us if (username === 'backend') { res.status(200).send('allow'); return; } const user = await User.findOne({ name: username.toString() }); if (user === null) { // Deny if user doesn't exist. res.status(200).send('deny'); } else { res.status(200).send('allow'); } } export async function Resource(req: Request, res: Response) { const username = req.query.username; const vhost = req.query.vhost; const resource = req.query.resource; const name = req.query.name; const permission = req.query.permission; const tags = req.query.tags; if (username === undefined || vhost === undefined || resource === undefined || name === undefined || permission === undefined || tags === undefined) { res.status(200).send('deny'); return; } // Check if it's us if (username.toString() == 'backend') { res.status(200).send('allow'); return; } // Deny if not root vhost if (vhost.toString() != '/') { res.status(200).send('deny'); return; } // Check if user exists const user = await User.findOne({ name: username.toString() }); if (user == null) { res.status(200).send('deny'); return; } if (tags.toString() == "administrator" && user.type != UserType.ADMIN) { res.status(200).send('deny'); return; } // TODO: This has to change if we want to allow users to see the realtime movement of others. if (resource.toString().startsWith('tracker-') && resource != 'tracker-' + username) { res.status(200).send('deny'); return; } res.status(200).send('allow'); } export async function Topic(req: Request, res: Response) { res.status(200); const username = req.query.username; const routingKey = req.query.routing_key; if (routingKey === undefined || username === undefined) { res.send('deny'); return; } // Check if it's us if (username.toString() == 'backend') { res.status(200).send('allow'); return; } // Check if user exists const user = await User.findOne({ name: username.toString() }); if (user === null) { res.send('deny'); return; } if (routingKey !== user.id) { res.send('deny'); return; } res.status(200).send('allow'); } /** * This middleware validates any tokens that are required to access most of the endpoints. * Note: This validation doesn't contain any permission checking. */ export async function MW_User(req: LivebeatRequest, res: Response, next: () => void) { if (req.headers.token === undefined) { res.status(401).send({ message: "Token not specified" }); return; } const token = req.headers.token.toString(); try { // Verify token if(await verify(token, JWT_SECRET, { algorithms: ['HS256'] })) { // Token is valid, now look if user is in db (in case he got deleted) const id = decode(token, { json: true })!.user; const db = await User.findById(id); if (db !== undefined && db !== null) { req.user = db next(); return; } else { res.status(401).send({ message: "Token is not valid" }); } } else { res.status(401).send({ message: "Token is not valid" }); } } catch (err) { if (err) { res.status(500).send({ message: "We failed validating your token for some reason." }); logger.error(err); } } }