diff --git a/lib/new-admin/graphql/modules/authentication.js b/lib/new-admin/graphql/modules/authentication.js index 9b9e4703..0ca3ed7a 100644 --- a/lib/new-admin/graphql/modules/authentication.js +++ b/lib/new-admin/graphql/modules/authentication.js @@ -10,8 +10,9 @@ const authErrors = require('../errors/authentication') const REMEMBER_ME_AGE = 90 * T.day function authenticateUser(username, password) { - return loginHelper.checkUser(username) - .then(hashedPassword => { + return users.getUserByUsername(username) + .then(user => { + const hashedPassword = user.password if (!hashedPassword) throw new authErrors.InvalidCredentialsError() return Promise.all([bcrypt.compare(password, hashedPassword), hashedPassword]) }) @@ -47,7 +48,7 @@ const confirm2FA = (token, context) => { if (!requestingUser) throw new authErrors.InvalidCredentialsError() - return users.get2FASecret(requestingUser.id).then(user => { + return users.getUserById(requestingUser.id).then(user => { const secret = user.twofa_code const isCodeValid = otplib.authenticator.verify({ token, secret }) @@ -81,7 +82,7 @@ const validateReset2FALink = token => { return users.validate2FAResetToken(token) .then(r => { if (!r.success) throw new authErrors.InvalidUrlError() - return users.findById(r.userID) + return users.getUserById(r.userID) }) .then(user => { const secret = otplib.authenticator.generateSecret() @@ -101,7 +102,7 @@ const deleteSession = (sessionID, context) => { const login = (username, password) => { return authenticateUser(username, password).then(user => { if (!user) throw new authErrors.InvalidCredentialsError() - return users.get2FASecret(user.id).then(user => { + return users.getUserById(user.id).then(user => { const twoFASecret = user.twofa_code return twoFASecret ? 'INPUT2FA' : 'SETUP2FA' }) @@ -112,7 +113,7 @@ const input2FA = (username, password, rememberMe, code, context) => { return authenticateUser(username, password).then(user => { if (!user) throw new authErrors.InvalidCredentialsError() - return users.get2FASecret(user.id).then(user => { + return users.getUserById(user.id).then(user => { const secret = user.twofa_code const isCodeValid = otplib.authenticator.verify({ token: code, secret: secret }) if (!isCodeValid) throw new authErrors.InvalidTwoFactorError() @@ -138,7 +139,7 @@ const setup2FA = (username, password, secret, codeConfirmation) => { } const createResetPasswordToken = userID => { - return users.findById(userID) + return users.getUserById(userID) .then(user => { if (!user) throw new authErrors.InvalidCredentialsError() return users.createResetPasswordToken(user.id) @@ -147,7 +148,7 @@ const createResetPasswordToken = userID => { } const createReset2FAToken = userID => { - return users.findById(userID) + return users.getUserById(userID) .then(user => { if (!user) throw new authErrors.InvalidCredentialsError() return users.createReset2FAToken(user.id) @@ -156,7 +157,7 @@ const createReset2FAToken = userID => { } const createRegisterToken = (username, role) => { - return users.getByName(username) + return users.getUserByUsername(username) .then(user => { if (user) throw new authErrors.UserAlreadyExistsError() @@ -165,29 +166,29 @@ const createRegisterToken = (username, role) => { .catch(err => console.error(err)) } -const register = (username, password, role) => { - return users.getByName(username) +const register = (token, username, password, role) => { + return users.getUserByUsername(username) .then(user => { if (user) throw new authErrors.UserAlreadyExistsError() - return users.createUser(username, password, role).then(() => true) + return users.register(token, username, password, role).then(() => true) }) .catch(err => console.error(err)) } -const resetPassword = (userID, newPassword, context) => { - return users.findById(userID).then(user => { +const resetPassword = (token, userID, newPassword, context) => { + return users.getUserById(userID).then(user => { if (!user) throw new authErrors.InvalidCredentialsError() if (context.req.session.user && user.id === context.req.session.user.id) context.req.session.destroy() - return users.updatePassword(user.id, newPassword) + return users.updatePassword(token, user.id, newPassword) }).then(() => true).catch(err => console.error(err)) } -const reset2FA = (userID, token, secret, context) => { - return users.findById(userID).then(user => { - const isCodeValid = otplib.authenticator.verify({ token, secret }) +const reset2FA = (token, userID, code, secret, context) => { + return users.getUserById(userID).then(user => { + const isCodeValid = otplib.authenticator.verify({ token: code, secret }) if (!isCodeValid) throw new authErrors.InvalidTwoFactorError() if (context.req.session.user && user.id === context.req.session.user.id) context.req.session.destroy() - return users.save2FASecret(user.id, secret).then(() => true) + return users.reset2FASecret(token, user.id, secret).then(() => true) }).catch(err => console.error(err)) } diff --git a/lib/new-admin/graphql/resolvers/users.resolver.js b/lib/new-admin/graphql/resolvers/users.resolver.js index c35d5eda..ad7fa0a7 100644 --- a/lib/new-admin/graphql/resolvers/users.resolver.js +++ b/lib/new-admin/graphql/resolvers/users.resolver.js @@ -15,20 +15,20 @@ const resolver = { validateReset2FALink: (...[, { token }]) => authentication.validateReset2FALink(token) }, Mutation: { - deleteUser: (...[, { id }]) => users.deleteUser(id), + enableUser: (...[, { id }]) => users.enableUser(id), + disableUser: (...[, { id }]) => users.disableUser(id), deleteSession: (root, args, context, info) => authentication.deleteSession(args.sid, context), deleteUserSessions: (...[, { username }]) => sessionManager.deleteSessionsByUsername(username), changeUserRole: (...[, { id, newRole }]) => users.changeUserRole(id, newRole), - toggleUserEnable: (...[, { id }]) => users.toggleUserEnable(id), login: (...[, { username, password }]) => authentication.login(username, password), input2FA: (root, args, context, info) => authentication.input2FA(args.username, args.password, args.rememberMe, args.code, context), setup2FA: (...[, { username, password, secret, codeConfirmation }]) => authentication.setup2FA(username, password, secret, codeConfirmation), createResetPasswordToken: (...[, { userID }]) => authentication.createResetPasswordToken(userID), createReset2FAToken: (...[, { userID }]) => authentication.createReset2FAToken(userID), createRegisterToken: (...[, { username, role }]) => authentication.createRegisterToken(username, role), - register: (...[, { username, password, role }]) => authentication.register(username, password, role), - resetPassword: (root, args, context, info) => authentication.resetPassword(args.userID, args.newPassword, context), - reset2FA: (root, args, context, info) => authentication.reset2FA(args.userID, args.code, args.secret, context) + register: (...[, { token, username, password, role }]) => authentication.register(token, username, password, role), + resetPassword: (root, args, context, info) => authentication.resetPassword(args.token, args.userID, args.newPassword, context), + reset2FA: (root, args, context, info) => authentication.reset2FA(args.token, args.userID, args.code, args.secret, context) } } diff --git a/lib/new-admin/graphql/types/users.type.js b/lib/new-admin/graphql/types/users.type.js index ee99b5fa..9ccc5a59 100644 --- a/lib/new-admin/graphql/types/users.type.js +++ b/lib/new-admin/graphql/types/users.type.js @@ -57,7 +57,8 @@ const typeDef = ` } type Mutation { - deleteUser(id: ID!): User @auth(requires: [SUPERUSER]) + enableUser(id: ID!): User @auth(requires: [SUPERUSER]) + disableUser(id: ID!): User @auth(requires: [SUPERUSER]) deleteSession(sid: String!): UserSession @auth(requires: [SUPERUSER]) deleteUserSessions(username: String!): [UserSession] @auth(requires: [SUPERUSER]) changeUserRole(id: ID!, newRole: String!): User @auth(requires: [SUPERUSER]) @@ -68,9 +69,9 @@ const typeDef = ` createResetPasswordToken(userID: ID!): ResetToken @auth(requires: [SUPERUSER]) createReset2FAToken(userID: ID!): ResetToken @auth(requires: [SUPERUSER]) createRegisterToken(username: String!, role: String!): RegistrationToken @auth(requires: [SUPERUSER]) - register(username: String!, password: String!, role: String!): Boolean - resetPassword(userID: ID!, newPassword: String!): Boolean - reset2FA(userID: ID!, secret: String!, code: String!): Boolean + register(token: String!, username: String!, password: String!, role: String!): Boolean + resetPassword(token: String!, userID: ID!, newPassword: String!): Boolean + reset2FA(token: String!, userID: ID!, secret: String!, code: String!): Boolean } ` diff --git a/lib/new-admin/services/login.js b/lib/new-admin/services/login.js index 9e763cab..2c8ca03e 100644 --- a/lib/new-admin/services/login.js +++ b/lib/new-admin/services/login.js @@ -1,10 +1,5 @@ const db = require('../../db') -function checkUser (username) { - const sql = 'SELECT * FROM users WHERE username=$1' - return db.oneOrNone(sql, [username]).then(value => { return value.password }).catch(() => false) -} - function validateUser (username, password) { const sql = 'SELECT id, username FROM users WHERE username=$1 AND password=$2' const sqlUpdateLastAccessed = 'UPDATE users SET last_accessed = now() WHERE username=$1' @@ -18,6 +13,5 @@ function validateUser (username, password) { } module.exports = { - checkUser, validateUser } diff --git a/lib/users.js b/lib/users.js index df785317..054ea3f8 100644 --- a/lib/users.js +++ b/lib/users.js @@ -37,16 +37,21 @@ function getByIds (ids) { return db.any(sql, [idList]) } +function getUserById (id) { + const sql = `SELECT * FROM users WHERE id=$1` + return db.oneOrNone(sql, [id]) +} + +function getUserByUsername (username) { + const sql = `SELECT * FROM users WHERE username=$1` + return db.oneOrNone(sql, [username]) +} + function getUsers () { const sql = `SELECT id, username, role, enabled, last_accessed, last_accessed_from, last_accessed_address FROM users ORDER BY username` return db.any(sql) } -function getByName (username) { - const sql = `SELECT id, username, role, last_accessed FROM users WHERE username=$1 limit 1` - return db.oneOrNone(sql, [username]) -} - function verifyAndUpdateUser (id, ua, ip) { const sql = `SELECT id, username, role, enabled FROM users WHERE id=$1 limit 1` return db.oneOrNone(sql, [id]).then(user => { @@ -59,83 +64,66 @@ function verifyAndUpdateUser (id, ua, ip) { }) } -function createUser (username, password, role) { - const sql = `INSERT INTO users (id, username, password, role) VALUES ($1, $2, $3, $4)` - return bcrypt.hash(password, 12).then(function (hash) { - return db.none(sql, [uuid.v4(), username, hash, role]) - }) -} - -// TO DELETE -function deleteUser (id) { - const sql = `DELETE FROM users WHERE id=$1` - const sql2 = `DELETE FROM user_sessions WHERE sess -> 'user' ->> 'id'=$1` - - return db.none(sql, [id]).then(() => db.none(sql2, [id])) -} - -function findById (id) { - const sql = 'SELECT id, username FROM users WHERE id=$1' - return db.oneOrNone(sql, [id]) -} - -function get2FASecret (id) { - const sql = 'SELECT id, username, twofa_code, role FROM users WHERE id=$1' - return db.oneOrNone(sql, [id]) -} - function save2FASecret (id, secret) { return db.tx(t => { const q1 = t.none('UPDATE users SET twofa_code=$1 WHERE id=$2', [secret, id]) const q2 = t.none(`DELETE FROM user_sessions WHERE sess -> 'user' ->> 'id'=$1`, [id]) return t.batch([q1, q2]) - // const sql = 'UPDATE users SET twofa_code=$1 WHERE id=$2' - // const sql2 = `DELETE FROM user_sessions WHERE sess -> 'user' ->> 'id'=$1` - // return db.none(sql, [secret, id]).then(() => db.none(sql2, [id])) }) } function validate2FAResetToken (token) { - const sql = `DELETE FROM reset_twofa - WHERE token=$1 - RETURNING user_id, now() < expire AS success` + const sql = `SELECT user_id, now() < expire AS success FROM auth_tokens + WHERE token=$1 AND type='reset_twofa'` return db.one(sql, [token]) .then(res => ({ userID: res.user_id, success: res.success })) } +function reset2FASecret (token, id, secret) { + return validate2FAResetToken(token).then(res => { + if (!res.success) throw new Error('Failed to verify 2FA reset token') + return db.tx(t => { + const q1 = t.none('UPDATE users SET twofa_code=$1 WHERE id=$2', [secret, id]) + const q2 = t.none(`DELETE FROM user_sessions WHERE sess -> 'user' ->> 'id'=$1`, [id]) + const q3 = t.none(`DELETE FROM auth_tokens WHERE token=$1 and type='reset_password'`, [token]) + return t.batch([q1, q2, q3]) + }) + }) +} + function createReset2FAToken (userID) { const token = crypto.randomBytes(32).toString('hex') - const sql = `INSERT INTO reset_twofa (token, user_id) VALUES ($1, $2) ON CONFLICT (user_id) DO UPDATE SET token=$1, expire=now() + interval '30 minutes' RETURNING *` + const sql = `INSERT INTO auth_tokens (token, type, user_id) VALUES ($1, 'reset_twofa', $2) ON CONFLICT (user_id) DO UPDATE SET token=$1, expire=now() + interval '30 minutes' RETURNING *` return db.one(sql, [token, userID]) } -function updatePassword (id, password) { - bcrypt.hash(password, 12).then(function (hash) { - return db.tx(t => { - const q1 = t.none(`UPDATE users SET password=$1 WHERE id=$2`, [hash, id]) - const q2 = t.none(`DELETE FROM user_sessions WHERE sess -> 'user' ->> 'id'=$1`, [id]) - return t.batch([q1, q2]) - }) - // const sql = `UPDATE users SET password=$1 WHERE id=$2` - // const sql2 = `DELETE FROM user_sessions WHERE sess -> 'user' ->> 'id'=$1` - // return db.none(sql, [hash, id]).then(() => db.none(sql2, [id])) - }) -} - function validatePasswordResetToken (token) { - const sql = `DELETE FROM reset_password - WHERE token=$1 - RETURNING user_id, now() < expire AS success` + const sql = `SELECT user_id, now() < expire AS success FROM auth_tokens + WHERE token=$1 AND type='reset_password'` return db.one(sql, [token]) .then(res => ({ userID: res.user_id, success: res.success })) } +function updatePassword (token, id, password) { + return validatePasswordResetToken(token).then(res => { + if (!res.success) throw new Error('Failed to verify password reset token') + return bcrypt.hash(password, 12).then(function (hash) { + return db.tx(t => { + const q1 = t.none(`UPDATE users SET password=$1 WHERE id=$2`, [hash, id]) + const q2 = t.none(`DELETE FROM user_sessions WHERE sess -> 'user' ->> 'id'=$1`, [id]) + const q3 = t.none(`DELETE FROM auth_tokens WHERE token=$1 and type='reset_password'`, [token]) + return t.batch([q1, q2, q3]) + }) + }) + }) +} + function createResetPasswordToken (userID) { const token = crypto.randomBytes(32).toString('hex') - const sql = `INSERT INTO reset_password (token, user_id) VALUES ($1, $2) ON CONFLICT (user_id) DO UPDATE SET token=$1, expire=now() + interval '30 minutes' RETURNING *` + const sql = `INSERT INTO auth_tokens (token, type, user_id) VALUES ($1, 'reset_password', $2) ON CONFLICT (user_id) DO UPDATE SET token=$1, expire=now() + interval '30 minutes' RETURNING *` return db.one(sql, [token, userID]) } @@ -149,20 +137,37 @@ function createUserRegistrationToken (username, role) { } function validateUserRegistrationToken (token) { - const sql = `DELETE FROM user_register_tokens WHERE token=$1 - RETURNING username, role, now() < expire AS success` + const sql = `SELECT username, role, now() < expire AS success FROM user_register_tokens WHERE token=$1` return db.one(sql, [token]) .then(res => ({ username: res.username, role: res.role, success: res.success })) } +function register (token, username, password, role) { + return validateUserRegistrationToken(token).then(res => { + if (!res.success) throw new Error('Failed to verify registration token') + return bcrypt.hash(password, 12).then(hash => { + return db.tx(t => { + const q1 = t.none(`INSERT INTO users (id, username, password, role) VALUES ($1, $2, $3, $4)`, [uuid.v4(), username, hash, role]) + const q2 = t.none(`DELETE FROM user_register_tokens WHERE token=$1`, [token]) + return t.batch([q1, q2]) + }) + }) + }) +} + function changeUserRole (id, newRole) { const sql = `UPDATE users SET role=$1 WHERE id=$2` return db.none(sql, [newRole, id]) } -function toggleUserEnable (id) { - const sql = `UPDATE users SET enabled=not enabled WHERE id=$1` +function enableUser (id) { + const sql = `UPDATE users SET enabled=true WHERE id=$1` + return db.none(sql, [id]) +} + +function disableUser (id) { + const sql = `UPDATE users SET enabled=false WHERE id=$1` return db.none(sql, [id]) } @@ -170,20 +175,20 @@ module.exports = { get, getByIds, getUsers, - getByName, + getUserById, + getUserByUsername, verifyAndUpdateUser, - createUser, - deleteUser, - findById, updatePassword, - get2FASecret, save2FASecret, + reset2FASecret, validate2FAResetToken, createReset2FAToken, validatePasswordResetToken, createResetPasswordToken, createUserRegistrationToken, validateUserRegistrationToken, + register, changeUserRole, - toggleUserEnable + enableUser, + disableUser } diff --git a/migrations/1605181184453-users.js b/migrations/1605181184453-users.js index 088ded55..4e1eeab7 100644 --- a/migrations/1605181184453-users.js +++ b/migrations/1605181184453-users.js @@ -21,18 +21,13 @@ exports.up = function (next) { WITH (OIDS=FALSE)`, `ALTER TABLE "user_sessions" ADD CONSTRAINT "session_pkey" PRIMARY KEY ("sid") NOT DEFERRABLE INITIALLY IMMEDIATE`, `CREATE INDEX "IDX_session_expire" ON "user_sessions" ("expire")`, - `CREATE TABLE reset_password ( + `CREATE TYPE auth_token_type AS ENUM('reset_password', 'reset_twofa')`, + `CREATE TABLE auth_tokens ( token TEXT NOT NULL PRIMARY KEY, + type auth_token_type NOT NULL, user_id UUID REFERENCES users(id) ON DELETE CASCADE UNIQUE, expire TIMESTAMPTZ NOT NULL DEFAULT now() + interval '30 minutes' )`, - `CREATE INDEX "idx_reset_pw_expire" ON "reset_password" ("expire")`, - `CREATE TABLE reset_twofa ( - token TEXT NOT NULL PRIMARY KEY, - user_id UUID REFERENCES users(id) ON DELETE CASCADE UNIQUE, - expire TIMESTAMPTZ NOT NULL DEFAULT now() + interval '30 minutes' - )`, - `CREATE INDEX "idx_reset_twofa_expire" ON "reset_twofa" ("expire")`, `CREATE TABLE user_register_tokens ( token TEXT NOT NULL PRIMARY KEY, username TEXT NOT NULL UNIQUE, diff --git a/new-lamassu-admin/src/components/layout/Header.js b/new-lamassu-admin/src/components/layout/Header.js index a476258f..d1716e1a 100644 --- a/new-lamassu-admin/src/components/layout/Header.js +++ b/new-lamassu-admin/src/components/layout/Header.js @@ -27,7 +27,7 @@ const HAS_UNREAD = gql` } ` -const Subheader = ({ item, classes }) => { +const Subheader = ({ item, classes, user }) => { const [prev, setPrev] = useState(null) return ( @@ -35,21 +35,32 @@ const Subheader = ({ item, classes }) => {