diff --git a/sites/backend/src/controllers/user.mjs b/sites/backend/src/controllers/user.mjs index acdca53255d..e1fbb6c3976 100644 --- a/sites/backend/src/controllers/user.mjs +++ b/sites/backend/src/controllers/user.mjs @@ -4,7 +4,7 @@ import jwt from 'jsonwebtoken' //import fs from 'fs' import Zip from 'jszip' //import rimraf from 'rimraf' -import { hash, hashPassword, randomString } from '../utils/crypto.mjs' +import { hash, hashPassword, randomString, verifyPassword } from '../utils/crypto.mjs' import { clean, asJson } from '../utils/index.mjs' import { log } from '../utils/log.mjs' import { emailTemplate } from '../utils/email.mjs' @@ -260,15 +260,41 @@ UserController.prototype.login = async function (req, res, tools) { }) } catch (err) { log.warn(err, `Error while trying to find username: ${req.body.username}`) - return res.status(401).send({ error: 'logingFailed', result }) + return res.status(401).send({ error: 'loginFailed', result }) } if (!account) { log.warn(`Login attempt for non-existing user: ${req.body.username} from ${req.ip}`) - return res.status(401).send({ error: 'logingFailed', result }) + return res.status(401).send({ error: 'loginFailed', result }) + } + + // Account found, check password + const [valid, updatedPasswordField] = verifyPassword(req.body.password, account.password) + if (!valid) { + log.warn(`Wrong password for existing user: ${req.body.username} from ${req.ip}`) + return res.status(401).send({ error: 'loginFailed', result }) } // Login success log.info(`Login by user ${account.id} (${account.username})`) + if (updatedPasswordField) { + // Update the password field with a v3 hash + let updateUser + try { + updateUser = await prisma.user.update({ + where: { + id: account.id, + }, + data: { + password: updatedPasswordField, + }, + }) + } catch (err) { + log.warn( + err, + `Could not update password field with v3 hash for user id ${account.id} (${account.username})` + ) + } + } return res.status(200).send({ result: 'success', diff --git a/sites/backend/src/utils/crypto.mjs b/sites/backend/src/utils/crypto.mjs index 4f4f5f818ef..6fe57d1db17 100644 --- a/sites/backend/src/utils/crypto.mjs +++ b/sites/backend/src/utils/crypto.mjs @@ -1,7 +1,7 @@ import bcrypt from 'bcryptjs' // Required for legacy password hashes import { createHash, createCipheriv, createDecipheriv, scryptSync, randomBytes } from 'crypto' import { log } from './log.mjs' -import { asJson, clean } from './index.mjs' +import { asJson } from './index.mjs' /* * Hashes an email address (or other string) @@ -14,7 +14,7 @@ export const hash = (string) => createHash('sha256').update(string).digest('hex' * This is not used in anything cryptographic. It is only used as a temporary * username to avoid username collisions */ -export const randomString = (bytes=8) => randomBytes(bytes).toString('hex') +export const randomString = (bytes = 8) => randomBytes(bytes).toString('hex') /* * Returns an object holding encrypt() and decrypt() methods diff --git a/sites/backend/src/utils/index.mjs b/sites/backend/src/utils/index.mjs index da5f63c4334..261cda98807 100644 --- a/sites/backend/src/utils/index.mjs +++ b/sites/backend/src/utils/index.mjs @@ -4,7 +4,8 @@ import { api } from '../config.mjs' * Cleans a string (typically email) for hashing */ export const clean = (string) => { - if (typeof string !== 'string') throw 'clean() only takes a string as input' + if (typeof string === 'number') string = string.toString() + if (typeof string !== 'string') throw 'clean() only takes a string or number as input' return string.toLowerCase().trim() } diff --git a/sites/backend/tests/user.test.mjs b/sites/backend/tests/user.test.mjs index db629f5f205..50cf5e096f5 100644 --- a/sites/backend/tests/user.test.mjs +++ b/sites/backend/tests/user.test.mjs @@ -87,6 +87,7 @@ describe(`${user} Signup flow and authentication`, () => { expect(typeof res.body.account.id).to.equal(`number`) store.token = res.body.token store.username = res.body.account.username + store.userid = res.body.account.id done() }) }) @@ -106,7 +107,141 @@ describe(`${user} Signup flow and authentication`, () => { }) }) + step(`${user} Should not login with the wrong password`, (done) => { + chai + .request(config.api) + .post('/login') + .send({ + username: store.username, + password: store.username, + }) + .end((err, res) => { + expect(res.status).to.equal(401) + expect(res.type).to.equal('application/json') + expect(res.charset).to.equal('utf-8') + expect(res.body.result).to.equal(`error`) + expect(res.body.error).to.equal(`loginFailed`) + done() + }) + }) + step(`${user} Should login with username and password`, (done) => { + chai + .request(config.api) + .post('/login') + .send({ + username: store.username, + password: data.password, + }) + .end((err, res) => { + expect(res.status).to.equal(200) + expect(res.type).to.equal('application/json') + expect(res.charset).to.equal('utf-8') + expect(res.body.result).to.equal(`success`) + expect(res.body.account.email).to.equal(data.email) + expect(res.body.account.username).to.equal(store.username) + expect(res.body.account.lusername).to.equal(store.username.toLowerCase()) + expect(typeof res.body.token).to.equal(`string`) + expect(typeof res.body.account.id).to.equal(`number`) + store.token = res.body.token + done() + }) + }) + + step(`${user} Should login with USERNAME and password`, (done) => { + chai + .request(config.api) + .post('/login') + .send({ + username: store.username.toUpperCase(), + password: data.password, + }) + .end((err, res) => { + expect(res.status).to.equal(200) + expect(res.type).to.equal('application/json') + expect(res.charset).to.equal('utf-8') + expect(res.body.result).to.equal(`success`) + expect(res.body.account.email).to.equal(data.email) + expect(res.body.account.username).to.equal(store.username) + expect(res.body.account.lusername).to.equal(store.username.toLowerCase()) + expect(typeof res.body.token).to.equal(`string`) + expect(typeof res.body.account.id).to.equal(`number`) + store.token = res.body.token + done() + }) + }) + + step(`${user} Should login with email and password`, (done) => { + chai + .request(config.api) + .post('/login') + .send({ + username: data.email, + password: data.password, + }) + .end((err, res) => { + expect(res.status).to.equal(200) + expect(res.type).to.equal('application/json') + expect(res.charset).to.equal('utf-8') + expect(res.body.result).to.equal(`success`) + expect(res.body.account.email).to.equal(data.email) + expect(res.body.account.username).to.equal(store.username) + expect(res.body.account.lusername).to.equal(store.username.toLowerCase()) + expect(typeof res.body.token).to.equal(`string`) + expect(typeof res.body.account.id).to.equal(`number`) + store.token = res.body.token + done() + }) + }) + + step(`${user} Should login with EMAIL and password`, (done) => { + chai + .request(config.api) + .post('/login') + .send({ + username: data.email.toUpperCase(), + password: data.password, + }) + .end((err, res) => { + expect(res.status).to.equal(200) + expect(res.type).to.equal('application/json') + expect(res.charset).to.equal('utf-8') + expect(res.body.result).to.equal(`success`) + expect(res.body.account.email).to.equal(data.email) + expect(res.body.account.username).to.equal(store.username) + expect(res.body.account.lusername).to.equal(store.username.toLowerCase()) + expect(typeof res.body.token).to.equal(`string`) + expect(typeof res.body.account.id).to.equal(`number`) + store.token = res.body.token + done() + }) + }) + + step(`${user} Should login with userid and password`, (done) => { + chai + .request(config.api) + .post('/login') + .send({ + username: store.userid, + password: data.password, + }) + .end((err, res) => { + expect(res.status).to.equal(200) + expect(res.type).to.equal('application/json') + expect(res.charset).to.equal('utf-8') + expect(res.body.result).to.equal(`success`) + expect(res.body.account.email).to.equal(data.email) + expect(res.body.account.username).to.equal(store.username) + expect(res.body.account.lusername).to.equal(store.username.toLowerCase()) + expect(typeof res.body.token).to.equal(`string`) + expect(typeof res.body.account.id).to.equal(`number`) + store.token = res.body.token + done() + }) + }) + + /* + step(`${user} Should login with email address and password`, (done) => { chai .request(config.api) .post('/login') @@ -129,27 +264,9 @@ describe(`${user} Signup flow and authentication`, () => { }) }) - /* describe('Login/Logout and session handling', () => { - it('should login with the username', (done) => { - chai - .request(config.backend) - .post('/login') - .send({ - username: config.user.username, - password: config.user.password, - }) - .end((err, res) => { - res.should.have.status(200) - let data = JSON.parse(res.text) - data.account.username.should.equal(config.user.username) - data.token.should.be.a('string') - config.user.token = data.token - done() - }) - }) it('should login with the email address', (done) => { chai