summaryrefslogtreecommitdiffstats
path: root/backend/src
diff options
context:
space:
mode:
Diffstat (limited to 'backend/src')
-rw-r--r--backend/src/authn.js139
-rw-r--r--backend/src/authz.js42
-rw-r--r--backend/src/permissions.js5
-rw-r--r--backend/src/routes.js141
4 files changed, 201 insertions, 126 deletions
diff --git a/backend/src/authn.js b/backend/src/authn.js
new file mode 100644
index 0000000..c6c4a2e
--- /dev/null
+++ b/backend/src/authn.js
@@ -0,0 +1,139 @@
+const base64 = require('base-64');
+const sha512 = require('js-sha512');
+const shortid = require('shortid');
+const db = require('./db');
+
+const buildSessionCookie = ({ username, sessionToken, isLogout }) => {
+ if (!isLogout) {
+ const tokenData = {
+ username,
+ sessionToken,
+ };
+ const tokenDataStr = JSON.stringify(tokenData);
+ const encodedToken = base64.encode(tokenDataStr);
+ return `sessionCookie=${encodedToken}; Domain=localhost; HttpOnly; SameSite=strict;`;
+ } else {
+ return `sessionCookie=deleted; Domain=localhost; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; SameSite=strict;`;
+ }
+};
+
+const authMiddleware = async (req, res, next) => {
+ const cookies = req.cookies;
+ const encodedCookie = cookies.sessionCookie;
+
+ if (!encodedCookie) {
+ res.status(403);
+ return res.json({
+ message: 'Log in first!',
+ });
+ }
+ const sessionCookieStr = base64.decode(encodedCookie);
+ const sessionCookie = JSON.parse(sessionCookieStr);
+
+ const { username, sessionToken } = sessionCookie;
+
+ const user = await db('users').where({ username }).first();
+ if (!user) {
+ res.status(400);
+ res.set('Set-Cookie', buildSessionCookie({ isLogout: true }));
+ return res.json({
+ message: 'Invalid session token.',
+ sessionCookie,
+ });
+ }
+
+ const salt = user.salt;
+
+ const sessionTokenHash = sha512(salt + sessionToken);
+ const token = await db('tokens').where({ type: 'session', value: sessionTokenHash });
+ if (!token) {
+ res.status(400);
+ res.set('Set-Cookie', buildSessionCookie({ isLogout: true }));
+ return res.json({
+ message: 'Invalid session token.',
+ sessionCookie,
+ });
+ }
+
+ req.sessionTokenHash = sessionTokenHash;
+ req.user = user;
+
+ next();
+};
+
+const registerRoute = async (req, res) => {
+ const { username, password } = req.body;
+ if (!username || !password) {
+ res.status(400);
+ return res.json({
+ error: 'Username or password missing.',
+ });
+ }
+
+ const existingUser = await db('users').where({ username }).first();
+ if (existingUser) {
+ res.status(400);
+ return res.json({
+ error: 'Username already taken.',
+ });
+ }
+
+ const salt = shortid.generate();
+ const pwHash = sha512(salt + password);
+
+ const sessionToken = shortid.generate();
+ const sessionTokenHash = sha512(salt + sessionToken);
+
+ const user = (await db('users').insert({ username, password: pwHash, salt }).returning('*'))[0];
+ await db('tokens').insert({ user_id: user.id, type: 'session', value: sessionTokenHash });
+
+ res.set('Set-Cookie', buildSessionCookie({ username, sessionToken, remember: false, isLogout: false }));
+ return res.json({
+ username,
+ });
+};
+
+const loginRoute = async (req, res) => {
+ const { username, password, remember } = req.body;
+ if (!username || !password) {
+ res.status(400);
+ return res.json({
+ error: 'Username or password missing.',
+ });
+ }
+
+ const user = await db('users').where({ username }).first();
+
+ if (user) {
+ const sessionToken = shortid.generate();
+ const sessionTokenHash = sha512(user.salt + sessionToken);
+
+ await db('tokens').insert({ user_id: user.id, type: 'session', value: sessionTokenHash });
+
+ res.set('Set-Cookie', buildSessionCookie({ username, sessionToken, remember, isLogout: false }));
+
+ return res.json({
+ username,
+ });
+ }
+
+ res.status(403);
+ res.json({
+ error: 'Wrong username or password',
+ });
+};
+
+const logoutRoute = async (req, res) => {
+ await db('tokens').where({ type: 'session', user_id: req.user.id, value: req.sessionTokenHash }).delete();
+ res.status(204);
+ res.set('Set-Cookie', buildSessionCookie({ isLogout: true }));
+ res.send();
+};
+
+module.exports = {
+ buildSessionCookie,
+ authMiddleware,
+ registerRoute,
+ loginRoute,
+ logoutRoute,
+};
diff --git a/backend/src/authz.js b/backend/src/authz.js
new file mode 100644
index 0000000..94dfa85
--- /dev/null
+++ b/backend/src/authz.js
@@ -0,0 +1,42 @@
+const db = require('./db');
+
+const userHasPermission = async (user, permission_name) => {
+ const permission = await db('permissions').where({ name: permission_name }).first();
+ const userPermission = await db('users_permissions')
+ .where({ user_id: user.id, permission_id: permission.id })
+ .first();
+
+ return !!userPermission;
+};
+
+const userHasPermissions = async (user, permission_names) => {
+ for (permission_name of permission_names) {
+ const hasPermission = await userHasPermission(user, permission_name);
+ if (!hasPermission) {
+ return false;
+ }
+ }
+ return true;
+};
+
+const permissionMiddleware = permission_name => async (req, res, next) => {
+ const user = req.user;
+ const hasPermission = Array.isArray(permission_name)
+ ? await userHasPermissions(user, permission_name)
+ : await userHasPermission(user, permission_name);
+
+ if (hasPermission) {
+ await next();
+ } else {
+ res.status(401);
+ res.json({
+ error: 'Required permission missing.',
+ permission_name,
+ });
+ }
+};
+
+module.exports = {
+ userHasPermission,
+ permissionMiddleware,
+};
diff --git a/backend/src/permissions.js b/backend/src/permissions.js
new file mode 100644
index 0000000..63cbb13
--- /dev/null
+++ b/backend/src/permissions.js
@@ -0,0 +1,5 @@
+const PERMISSIONS = {
+ READ_USERS: 'READ_USERS',
+};
+
+module.exports = PERMISSIONS;
diff --git a/backend/src/routes.js b/backend/src/routes.js
index b0f3660..9d6775e 100644
--- a/backend/src/routes.js
+++ b/backend/src/routes.js
@@ -1,69 +1,15 @@
const express = require('express');
-const sha512 = require('js-sha512');
-const shortid = require('shortid');
-const base64 = require('base-64');
const db = require('./db');
+const { permissionMiddleware } = require('./authz');
+const PERMISSIONS = require('./permissions');
-const buildSessionCookie = ({ username, sessionToken, isLogout, remember }) => {
- if (!isLogout) {
- const joinedToken = `${username}&${sessionToken}`;
- const encodedToken = base64.encode(joinedToken);
- if (remember) {
- return `sessionCookie=${encodedToken}; Domain=localhost; HttpOnly; SameSite=strict;`;
- } else {
- return `sessionCookie=${encodedToken}; Domain=localhost; HttpOnly; SameSite=strict; Max-Age=86400;`;
- }
- } else {
- return `sessionCookie=deleted; Domain=localhost; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; SameSite=strict;`;
- }
-};
+const { authMiddleware, registerRoute, loginRoute, logoutRoute } = require('./authn');
const normalRouter = express.Router();
const authedRouter = express.Router();
-authedRouter.use(async (req, res, next) => {
- const cookies = req.cookies;
- const encodedCookie = cookies.sessionCookie;
-
- if (!encodedCookie) {
- res.status(403);
- return res.json({
- message: 'Log in first!',
- });
- }
- const sessionCookie = base64.decode(encodedCookie);
-
- const [ username, sessionToken ] = sessionCookie.split('&');
-
- const user = await db('users').where({ username }).first();
- if (!user) {
- res.status(400);
- res.set('Set-Cookie', buildSessionCookie({ isLogout: true }));
- return res.json({
- message: 'Invalid session token.',
- sessionCookie,
- });
- }
-
- const salt = user.salt;
-
- const sessionTokenHash = sha512(salt + sessionToken);
- const token = await db('tokens').where({ type: 'session', value: sessionTokenHash });
- if (!token) {
- res.status(400);
- res.set('Set-Cookie', buildSessionCookie({ isLogout: true }));
- return res.json({
- message: 'Invalid session token.',
- sessionCookie,
- });
- }
-
- req.sessionTokenHash = sessionTokenHash;
- req.user = user;
-
- next();
-});
+authedRouter.use(authMiddleware);
normalRouter.get('/', (req, res) => {
res.json({
@@ -71,74 +17,10 @@ normalRouter.get('/', (req, res) => {
});
});
-normalRouter.post('/register', async (req, res) => {
- const { username, password } = req.body;
- if (!username || !password) {
- res.status(400);
- return res.json({
- error: 'Username or password missing.',
- });
- }
-
- const existingUser = await db('users').where({ username }).first();
- if (existingUser) {
- res.status(400);
- return res.json({
- error: 'Username already taken.',
- });
- }
-
- const salt = shortid.generate();
- const pwHash = sha512(salt + password);
-
- const sessionToken = shortid.generate();
- const sessionTokenHash = sha512(salt + sessionToken);
-
- const user = (await db('users').insert({ username, password: pwHash, salt }).returning('*'))[0];
- await db('tokens').insert({ user_id: user.id, type: 'session', value: sessionTokenHash });
-
- res.set('Set-Cookie', buildSessionCookie({ username, sessionToken, remember: false, isLogout: false }));
- return res.json({
- username,
- });
-});
-
-normalRouter.post('/login', async (req, res) => {
- const { username, password, remember } = req.body;
- if (!username || !password) {
- res.status(400);
- return res.json({
- error: 'Username or password missing.',
- });
- }
-
- const user = await db('users').where({ username }).first();
-
- if (user) {
- const sessionToken = shortid.generate();
- const sessionTokenHash = sha512(user.salt + sessionToken);
-
- await db('tokens').insert({ user_id: user.id, type: 'session', value: sessionTokenHash });
-
- res.set('Set-Cookie', buildSessionCookie({ username, sessionToken, remember, isLogout: false }));
-
- return res.json({
- username,
- });
- }
-
- res.status(403);
- res.json({
- error: 'Wrong username or password',
- });
-});
-
-authedRouter.post('/logout', async (req, res) => {
- await db('tokens').where({ type: 'session', user_id: req.user.id, value: req.sessionTokenHash }).delete();
- res.status(204);
- res.set('Set-Cookie', buildSessionCookie({ isLogout: true }));
- res.send();
-});
+/* Authentication flow */
+normalRouter.post('/register', registerRoute);
+normalRouter.post('/login', loginRoute);
+authedRouter.post('/logout', logoutRoute);
authedRouter.get('/user', (req, res) => {
const user = req.user;
@@ -148,4 +30,11 @@ authedRouter.get('/user', (req, res) => {
});
});
+authedRouter.get('/users', permissionMiddleware(PERMISSIONS.READ_USERS), async (req, res) => {
+ const users = await db('users').select('id', 'username', 'created_at');
+ return res.json({
+ users,
+ });
+});
+
module.exports = { authedRouter, normalRouter };