From 2011d3f6f9938825a36670779fa0d7894bda338f Mon Sep 17 00:00:00 2001 From: Jan Tuomi Date: Wed, 5 Feb 2020 11:35:03 +0200 Subject: Implement session tokens securely --- backend/migrations/20200201145548_password-salt.js | 11 ++++ backend/package-lock.json | 5 ++ backend/package.json | 1 + backend/src/routes.js | 68 ++++++++++++++-------- 4 files changed, 60 insertions(+), 25 deletions(-) create mode 100644 backend/migrations/20200201145548_password-salt.js (limited to 'backend') diff --git a/backend/migrations/20200201145548_password-salt.js b/backend/migrations/20200201145548_password-salt.js new file mode 100644 index 0000000..217b02b --- /dev/null +++ b/backend/migrations/20200201145548_password-salt.js @@ -0,0 +1,11 @@ +exports.up = function(knex) { + return knex.schema.table('users', function(table) { + table.string('salt').notNullable(); + }); +}; + +exports.down = function(knex) { + return knex.schema.table('users', function(table) { + table.dropColumn('salt'); + }); +}; diff --git a/backend/package-lock.json b/backend/package-lock.json index ab6d9bf..6d7cd28 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -154,6 +154,11 @@ } } }, + "base-64": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", + "integrity": "sha1-eAqZyE59YAJgNhURxId2E78k9rs=" + }, "basic-auth": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", diff --git a/backend/package.json b/backend/package.json index 0883c00..e6b64b3 100644 --- a/backend/package.json +++ b/backend/package.json @@ -12,6 +12,7 @@ "author": "", "license": "ISC", "dependencies": { + "base-64": "^0.1.0", "body-parser": "^1.19.0", "cookie-parser": "^1.4.4", "express": "^4.17.1", diff --git a/backend/src/routes.js b/backend/src/routes.js index a762d3a..b0f3660 100644 --- a/backend/src/routes.js +++ b/backend/src/routes.js @@ -1,20 +1,21 @@ const express = require('express'); const sha512 = require('js-sha512'); const shortid = require('shortid'); - -const salt = 'suola'; +const base64 = require('base-64'); const db = require('./db'); -const buildSessionCookie = ({ sessionId, isLogout, remember }) => { +const buildSessionCookie = ({ username, sessionToken, isLogout, remember }) => { if (!isLogout) { + const joinedToken = `${username}&${sessionToken}`; + const encodedToken = base64.encode(joinedToken); if (remember) { - return `sessionId=${sessionId}; Domain=localhost; HttpOnly; SameSite=strict;`; + return `sessionCookie=${encodedToken}; Domain=localhost; HttpOnly; SameSite=strict;`; } else { - return `sessionId=${sessionId}; Domain=localhost; HttpOnly; SameSite=strict; Max-Age=86400;`; + return `sessionCookie=${encodedToken}; Domain=localhost; HttpOnly; SameSite=strict; Max-Age=86400;`; } } else { - return `sessionId=deleted; Domain=localhost; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; SameSite=strict;`; + return `sessionCookie=deleted; Domain=localhost; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; SameSite=strict;`; } }; @@ -23,29 +24,42 @@ const authedRouter = express.Router(); authedRouter.use(async (req, res, next) => { const cookies = req.cookies; - const { sessionId } = cookies; + const encodedCookie = cookies.sessionCookie; - if (!sessionId) { + if (!encodedCookie) { res.status(403); return res.json({ message: 'Log in first!', }); } + const sessionCookie = base64.decode(encodedCookie); - const token = await db('tokens').where('value', sessionId).first(); + const [ username, sessionToken ] = sessionCookie.split('&'); - if (!token) { + 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.', - sessionId, + sessionCookie, }); } - const user = await db('users').where({ id: token.user_id }).first(); + const salt = user.salt; - req.sessionId = sessionId; + 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(); @@ -66,20 +80,24 @@ normalRouter.post('/register', async (req, res) => { }); } - const existingUuser = await db('users').where({ username }).first(); - if (existingUuser) { + 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 sessionId = shortid.generate(); - const user = (await db('users').insert({ username, password: pwHash }).returning('*'))[0]; - await db('tokens').insert({ user_id: user.id, type: 'session', value: sessionId }); - res.set('Set-Cookie', buildSessionCookie({ sessionId, remember: false, isLogout: false })); + 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, }); @@ -97,10 +115,12 @@ normalRouter.post('/login', async (req, res) => { const user = await db('users').where({ username }).first(); if (user) { - const sessionId = shortid.generate(); - await db('tokens').insert({ user_id: user.id, type: 'session', value: sessionId }); + 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({ sessionId, remember, isLogout: false })); + res.set('Set-Cookie', buildSessionCookie({ username, sessionToken, remember, isLogout: false })); return res.json({ username, @@ -114,9 +134,7 @@ normalRouter.post('/login', async (req, res) => { }); authedRouter.post('/logout', async (req, res) => { - if (req.user && req.sessionId) { - await db('tokens').where({ value: req.sessionId }).delete(); - } + 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(); -- cgit v1.3