fix: security flaw on auth tokens, error handling
This commit is contained in:
parent
40974dd501
commit
c00249586d
12 changed files with 185 additions and 144 deletions
|
|
@ -10,8 +10,9 @@ const authErrors = require('../errors/authentication')
|
||||||
const REMEMBER_ME_AGE = 90 * T.day
|
const REMEMBER_ME_AGE = 90 * T.day
|
||||||
|
|
||||||
function authenticateUser(username, password) {
|
function authenticateUser(username, password) {
|
||||||
return loginHelper.checkUser(username)
|
return users.getUserByUsername(username)
|
||||||
.then(hashedPassword => {
|
.then(user => {
|
||||||
|
const hashedPassword = user.password
|
||||||
if (!hashedPassword) throw new authErrors.InvalidCredentialsError()
|
if (!hashedPassword) throw new authErrors.InvalidCredentialsError()
|
||||||
return Promise.all([bcrypt.compare(password, hashedPassword), hashedPassword])
|
return Promise.all([bcrypt.compare(password, hashedPassword), hashedPassword])
|
||||||
})
|
})
|
||||||
|
|
@ -47,7 +48,7 @@ const confirm2FA = (token, context) => {
|
||||||
|
|
||||||
if (!requestingUser) throw new authErrors.InvalidCredentialsError()
|
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 secret = user.twofa_code
|
||||||
const isCodeValid = otplib.authenticator.verify({ token, secret })
|
const isCodeValid = otplib.authenticator.verify({ token, secret })
|
||||||
|
|
||||||
|
|
@ -81,7 +82,7 @@ const validateReset2FALink = token => {
|
||||||
return users.validate2FAResetToken(token)
|
return users.validate2FAResetToken(token)
|
||||||
.then(r => {
|
.then(r => {
|
||||||
if (!r.success) throw new authErrors.InvalidUrlError()
|
if (!r.success) throw new authErrors.InvalidUrlError()
|
||||||
return users.findById(r.userID)
|
return users.getUserById(r.userID)
|
||||||
})
|
})
|
||||||
.then(user => {
|
.then(user => {
|
||||||
const secret = otplib.authenticator.generateSecret()
|
const secret = otplib.authenticator.generateSecret()
|
||||||
|
|
@ -101,7 +102,7 @@ const deleteSession = (sessionID, context) => {
|
||||||
const login = (username, password) => {
|
const login = (username, password) => {
|
||||||
return authenticateUser(username, password).then(user => {
|
return authenticateUser(username, password).then(user => {
|
||||||
if (!user) throw new authErrors.InvalidCredentialsError()
|
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
|
const twoFASecret = user.twofa_code
|
||||||
return twoFASecret ? 'INPUT2FA' : 'SETUP2FA'
|
return twoFASecret ? 'INPUT2FA' : 'SETUP2FA'
|
||||||
})
|
})
|
||||||
|
|
@ -112,7 +113,7 @@ const input2FA = (username, password, rememberMe, code, context) => {
|
||||||
return authenticateUser(username, password).then(user => {
|
return authenticateUser(username, password).then(user => {
|
||||||
if (!user) throw new authErrors.InvalidCredentialsError()
|
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 secret = user.twofa_code
|
||||||
const isCodeValid = otplib.authenticator.verify({ token: code, secret: secret })
|
const isCodeValid = otplib.authenticator.verify({ token: code, secret: secret })
|
||||||
if (!isCodeValid) throw new authErrors.InvalidTwoFactorError()
|
if (!isCodeValid) throw new authErrors.InvalidTwoFactorError()
|
||||||
|
|
@ -138,7 +139,7 @@ const setup2FA = (username, password, secret, codeConfirmation) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const createResetPasswordToken = userID => {
|
const createResetPasswordToken = userID => {
|
||||||
return users.findById(userID)
|
return users.getUserById(userID)
|
||||||
.then(user => {
|
.then(user => {
|
||||||
if (!user) throw new authErrors.InvalidCredentialsError()
|
if (!user) throw new authErrors.InvalidCredentialsError()
|
||||||
return users.createResetPasswordToken(user.id)
|
return users.createResetPasswordToken(user.id)
|
||||||
|
|
@ -147,7 +148,7 @@ const createResetPasswordToken = userID => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const createReset2FAToken = userID => {
|
const createReset2FAToken = userID => {
|
||||||
return users.findById(userID)
|
return users.getUserById(userID)
|
||||||
.then(user => {
|
.then(user => {
|
||||||
if (!user) throw new authErrors.InvalidCredentialsError()
|
if (!user) throw new authErrors.InvalidCredentialsError()
|
||||||
return users.createReset2FAToken(user.id)
|
return users.createReset2FAToken(user.id)
|
||||||
|
|
@ -156,7 +157,7 @@ const createReset2FAToken = userID => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const createRegisterToken = (username, role) => {
|
const createRegisterToken = (username, role) => {
|
||||||
return users.getByName(username)
|
return users.getUserByUsername(username)
|
||||||
.then(user => {
|
.then(user => {
|
||||||
if (user) throw new authErrors.UserAlreadyExistsError()
|
if (user) throw new authErrors.UserAlreadyExistsError()
|
||||||
|
|
||||||
|
|
@ -165,29 +166,29 @@ const createRegisterToken = (username, role) => {
|
||||||
.catch(err => console.error(err))
|
.catch(err => console.error(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
const register = (username, password, role) => {
|
const register = (token, username, password, role) => {
|
||||||
return users.getByName(username)
|
return users.getUserByUsername(username)
|
||||||
.then(user => {
|
.then(user => {
|
||||||
if (user) throw new authErrors.UserAlreadyExistsError()
|
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))
|
.catch(err => console.error(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
const resetPassword = (userID, newPassword, context) => {
|
const resetPassword = (token, userID, newPassword, context) => {
|
||||||
return users.findById(userID).then(user => {
|
return users.getUserById(userID).then(user => {
|
||||||
if (!user) throw new authErrors.InvalidCredentialsError()
|
if (!user) throw new authErrors.InvalidCredentialsError()
|
||||||
if (context.req.session.user && user.id === context.req.session.user.id) context.req.session.destroy()
|
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))
|
}).then(() => true).catch(err => console.error(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
const reset2FA = (userID, token, secret, context) => {
|
const reset2FA = (token, userID, code, secret, context) => {
|
||||||
return users.findById(userID).then(user => {
|
return users.getUserById(userID).then(user => {
|
||||||
const isCodeValid = otplib.authenticator.verify({ token, secret })
|
const isCodeValid = otplib.authenticator.verify({ token: code, secret })
|
||||||
if (!isCodeValid) throw new authErrors.InvalidTwoFactorError()
|
if (!isCodeValid) throw new authErrors.InvalidTwoFactorError()
|
||||||
if (context.req.session.user && user.id === context.req.session.user.id) context.req.session.destroy()
|
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))
|
}).catch(err => console.error(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,20 +15,20 @@ const resolver = {
|
||||||
validateReset2FALink: (...[, { token }]) => authentication.validateReset2FALink(token)
|
validateReset2FALink: (...[, { token }]) => authentication.validateReset2FALink(token)
|
||||||
},
|
},
|
||||||
Mutation: {
|
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),
|
deleteSession: (root, args, context, info) => authentication.deleteSession(args.sid, context),
|
||||||
deleteUserSessions: (...[, { username }]) => sessionManager.deleteSessionsByUsername(username),
|
deleteUserSessions: (...[, { username }]) => sessionManager.deleteSessionsByUsername(username),
|
||||||
changeUserRole: (...[, { id, newRole }]) => users.changeUserRole(id, newRole),
|
changeUserRole: (...[, { id, newRole }]) => users.changeUserRole(id, newRole),
|
||||||
toggleUserEnable: (...[, { id }]) => users.toggleUserEnable(id),
|
|
||||||
login: (...[, { username, password }]) => authentication.login(username, password),
|
login: (...[, { username, password }]) => authentication.login(username, password),
|
||||||
input2FA: (root, args, context, info) => authentication.input2FA(args.username, args.password, args.rememberMe, args.code, context),
|
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),
|
setup2FA: (...[, { username, password, secret, codeConfirmation }]) => authentication.setup2FA(username, password, secret, codeConfirmation),
|
||||||
createResetPasswordToken: (...[, { userID }]) => authentication.createResetPasswordToken(userID),
|
createResetPasswordToken: (...[, { userID }]) => authentication.createResetPasswordToken(userID),
|
||||||
createReset2FAToken: (...[, { userID }]) => authentication.createReset2FAToken(userID),
|
createReset2FAToken: (...[, { userID }]) => authentication.createReset2FAToken(userID),
|
||||||
createRegisterToken: (...[, { username, role }]) => authentication.createRegisterToken(username, role),
|
createRegisterToken: (...[, { username, role }]) => authentication.createRegisterToken(username, role),
|
||||||
register: (...[, { username, password, role }]) => authentication.register(username, password, role),
|
register: (...[, { token, username, password, role }]) => authentication.register(token, username, password, role),
|
||||||
resetPassword: (root, args, context, info) => authentication.resetPassword(args.userID, args.newPassword, context),
|
resetPassword: (root, args, context, info) => authentication.resetPassword(args.token, args.userID, args.newPassword, context),
|
||||||
reset2FA: (root, args, context, info) => authentication.reset2FA(args.userID, args.code, args.secret, context)
|
reset2FA: (root, args, context, info) => authentication.reset2FA(args.token, args.userID, args.code, args.secret, context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,8 @@ const typeDef = `
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mutation {
|
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])
|
deleteSession(sid: String!): UserSession @auth(requires: [SUPERUSER])
|
||||||
deleteUserSessions(username: String!): [UserSession] @auth(requires: [SUPERUSER])
|
deleteUserSessions(username: String!): [UserSession] @auth(requires: [SUPERUSER])
|
||||||
changeUserRole(id: ID!, newRole: String!): User @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])
|
createResetPasswordToken(userID: ID!): ResetToken @auth(requires: [SUPERUSER])
|
||||||
createReset2FAToken(userID: ID!): ResetToken @auth(requires: [SUPERUSER])
|
createReset2FAToken(userID: ID!): ResetToken @auth(requires: [SUPERUSER])
|
||||||
createRegisterToken(username: String!, role: String!): RegistrationToken @auth(requires: [SUPERUSER])
|
createRegisterToken(username: String!, role: String!): RegistrationToken @auth(requires: [SUPERUSER])
|
||||||
register(username: String!, password: String!, role: String!): Boolean
|
register(token: String!, username: String!, password: String!, role: String!): Boolean
|
||||||
resetPassword(userID: ID!, newPassword: String!): Boolean
|
resetPassword(token: String!, userID: ID!, newPassword: String!): Boolean
|
||||||
reset2FA(userID: ID!, secret: String!, code: String!): Boolean
|
reset2FA(token: String!, userID: ID!, secret: String!, code: String!): Boolean
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,5 @@
|
||||||
const db = require('../../db')
|
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) {
|
function validateUser (username, password) {
|
||||||
const sql = 'SELECT id, username FROM users WHERE username=$1 AND password=$2'
|
const sql = 'SELECT id, username FROM users WHERE username=$1 AND password=$2'
|
||||||
const sqlUpdateLastAccessed = 'UPDATE users SET last_accessed = now() WHERE username=$1'
|
const sqlUpdateLastAccessed = 'UPDATE users SET last_accessed = now() WHERE username=$1'
|
||||||
|
|
@ -18,6 +13,5 @@ function validateUser (username, password) {
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
checkUser,
|
|
||||||
validateUser
|
validateUser
|
||||||
}
|
}
|
||||||
|
|
|
||||||
133
lib/users.js
133
lib/users.js
|
|
@ -37,16 +37,21 @@ function getByIds (ids) {
|
||||||
return db.any(sql, [idList])
|
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 () {
|
function getUsers () {
|
||||||
const sql = `SELECT id, username, role, enabled, last_accessed, last_accessed_from, last_accessed_address FROM users ORDER BY username`
|
const sql = `SELECT id, username, role, enabled, last_accessed, last_accessed_from, last_accessed_address FROM users ORDER BY username`
|
||||||
return db.any(sql)
|
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) {
|
function verifyAndUpdateUser (id, ua, ip) {
|
||||||
const sql = `SELECT id, username, role, enabled FROM users WHERE id=$1 limit 1`
|
const sql = `SELECT id, username, role, enabled FROM users WHERE id=$1 limit 1`
|
||||||
return db.oneOrNone(sql, [id]).then(user => {
|
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) {
|
function save2FASecret (id, secret) {
|
||||||
return db.tx(t => {
|
return db.tx(t => {
|
||||||
const q1 = t.none('UPDATE users SET twofa_code=$1 WHERE id=$2', [secret, id])
|
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 q2 = t.none(`DELETE FROM user_sessions WHERE sess -> 'user' ->> 'id'=$1`, [id])
|
||||||
return t.batch([q1, q2])
|
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) {
|
function validate2FAResetToken (token) {
|
||||||
const sql = `DELETE FROM reset_twofa
|
const sql = `SELECT user_id, now() < expire AS success FROM auth_tokens
|
||||||
WHERE token=$1
|
WHERE token=$1 AND type='reset_twofa'`
|
||||||
RETURNING user_id, now() < expire AS success`
|
|
||||||
|
|
||||||
return db.one(sql, [token])
|
return db.one(sql, [token])
|
||||||
.then(res => ({ userID: res.user_id, success: res.success }))
|
.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) {
|
function createReset2FAToken (userID) {
|
||||||
const token = crypto.randomBytes(32).toString('hex')
|
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])
|
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) {
|
function validatePasswordResetToken (token) {
|
||||||
const sql = `DELETE FROM reset_password
|
const sql = `SELECT user_id, now() < expire AS success FROM auth_tokens
|
||||||
WHERE token=$1
|
WHERE token=$1 AND type='reset_password'`
|
||||||
RETURNING user_id, now() < expire AS success`
|
|
||||||
|
|
||||||
return db.one(sql, [token])
|
return db.one(sql, [token])
|
||||||
.then(res => ({ userID: res.user_id, success: res.success }))
|
.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) {
|
function createResetPasswordToken (userID) {
|
||||||
const token = crypto.randomBytes(32).toString('hex')
|
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])
|
return db.one(sql, [token, userID])
|
||||||
}
|
}
|
||||||
|
|
@ -149,20 +137,37 @@ function createUserRegistrationToken (username, role) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateUserRegistrationToken (token) {
|
function validateUserRegistrationToken (token) {
|
||||||
const sql = `DELETE FROM user_register_tokens WHERE token=$1
|
const sql = `SELECT username, role, now() < expire AS success FROM user_register_tokens WHERE token=$1`
|
||||||
RETURNING username, role, now() < expire AS success`
|
|
||||||
|
|
||||||
return db.one(sql, [token])
|
return db.one(sql, [token])
|
||||||
.then(res => ({ username: res.username, role: res.role, success: res.success }))
|
.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) {
|
function changeUserRole (id, newRole) {
|
||||||
const sql = `UPDATE users SET role=$1 WHERE id=$2`
|
const sql = `UPDATE users SET role=$1 WHERE id=$2`
|
||||||
return db.none(sql, [newRole, id])
|
return db.none(sql, [newRole, id])
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleUserEnable (id) {
|
function enableUser (id) {
|
||||||
const sql = `UPDATE users SET enabled=not enabled WHERE id=$1`
|
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])
|
return db.none(sql, [id])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -170,20 +175,20 @@ module.exports = {
|
||||||
get,
|
get,
|
||||||
getByIds,
|
getByIds,
|
||||||
getUsers,
|
getUsers,
|
||||||
getByName,
|
getUserById,
|
||||||
|
getUserByUsername,
|
||||||
verifyAndUpdateUser,
|
verifyAndUpdateUser,
|
||||||
createUser,
|
|
||||||
deleteUser,
|
|
||||||
findById,
|
|
||||||
updatePassword,
|
updatePassword,
|
||||||
get2FASecret,
|
|
||||||
save2FASecret,
|
save2FASecret,
|
||||||
|
reset2FASecret,
|
||||||
validate2FAResetToken,
|
validate2FAResetToken,
|
||||||
createReset2FAToken,
|
createReset2FAToken,
|
||||||
validatePasswordResetToken,
|
validatePasswordResetToken,
|
||||||
createResetPasswordToken,
|
createResetPasswordToken,
|
||||||
createUserRegistrationToken,
|
createUserRegistrationToken,
|
||||||
validateUserRegistrationToken,
|
validateUserRegistrationToken,
|
||||||
|
register,
|
||||||
changeUserRole,
|
changeUserRole,
|
||||||
toggleUserEnable
|
enableUser,
|
||||||
|
disableUser
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,18 +21,13 @@ exports.up = function (next) {
|
||||||
WITH (OIDS=FALSE)`,
|
WITH (OIDS=FALSE)`,
|
||||||
`ALTER TABLE "user_sessions" ADD CONSTRAINT "session_pkey" PRIMARY KEY ("sid") NOT DEFERRABLE INITIALLY IMMEDIATE`,
|
`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 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,
|
token TEXT NOT NULL PRIMARY KEY,
|
||||||
|
type auth_token_type NOT NULL,
|
||||||
user_id UUID REFERENCES users(id) ON DELETE CASCADE UNIQUE,
|
user_id UUID REFERENCES users(id) ON DELETE CASCADE UNIQUE,
|
||||||
expire TIMESTAMPTZ NOT NULL DEFAULT now() + interval '30 minutes'
|
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 (
|
`CREATE TABLE user_register_tokens (
|
||||||
token TEXT NOT NULL PRIMARY KEY,
|
token TEXT NOT NULL PRIMARY KEY,
|
||||||
username TEXT NOT NULL UNIQUE,
|
username TEXT NOT NULL UNIQUE,
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ const HAS_UNREAD = gql`
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const Subheader = ({ item, classes }) => {
|
const Subheader = ({ item, classes, user }) => {
|
||||||
const [prev, setPrev] = useState(null)
|
const [prev, setPrev] = useState(null)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -35,21 +35,32 @@ const Subheader = ({ item, classes }) => {
|
||||||
<div className={classes.content}>
|
<div className={classes.content}>
|
||||||
<nav>
|
<nav>
|
||||||
<ul className={classes.subheaderUl}>
|
<ul className={classes.subheaderUl}>
|
||||||
{item.children.map((it, idx) => (
|
{item.children.map((it, idx) => {
|
||||||
<li key={idx} className={classes.subheaderLi}>
|
if (
|
||||||
<NavLink
|
!R.includes(
|
||||||
to={{ pathname: it.route, state: { prev } }}
|
user.role,
|
||||||
className={classes.subheaderLink}
|
it.allowedRoles.map(v => {
|
||||||
activeClassName={classes.activeSubheaderLink}
|
return v.key
|
||||||
isActive={match => {
|
})
|
||||||
if (!match) return false
|
)
|
||||||
setPrev(it.route)
|
)
|
||||||
return true
|
return <></>
|
||||||
}}>
|
return (
|
||||||
{it.label}
|
<li key={idx} className={classes.subheaderLi}>
|
||||||
</NavLink>
|
<NavLink
|
||||||
</li>
|
to={{ pathname: it.route, state: { prev } }}
|
||||||
))}
|
className={classes.subheaderLink}
|
||||||
|
activeClassName={classes.activeSubheaderLink}
|
||||||
|
isActive={match => {
|
||||||
|
if (!match) return false
|
||||||
|
setPrev(it.route)
|
||||||
|
return true
|
||||||
|
}}>
|
||||||
|
{it.label}
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -193,7 +204,7 @@ const Header = memo(({ tree, user }) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{active && active.children && (
|
{active && active.children && (
|
||||||
<Subheader item={active} classes={classes} />
|
<Subheader item={active} classes={classes} user={user} />
|
||||||
)}
|
)}
|
||||||
{open && <AddMachine close={() => setOpen(false)} onPaired={onPaired} />}
|
{open && <AddMachine close={() => setOpen(false)} onPaired={onPaired} />}
|
||||||
</header>
|
</header>
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,18 @@ const VALIDATE_REGISTER_LINK = gql`
|
||||||
`
|
`
|
||||||
|
|
||||||
const REGISTER = gql`
|
const REGISTER = gql`
|
||||||
mutation register($username: String!, $password: String!, $role: String!) {
|
mutation register(
|
||||||
register(username: $username, password: $password, role: $role)
|
$token: String!
|
||||||
|
$username: String!
|
||||||
|
$password: String!
|
||||||
|
$role: String!
|
||||||
|
) {
|
||||||
|
register(
|
||||||
|
token: $token
|
||||||
|
username: $username
|
||||||
|
password: $password
|
||||||
|
role: $role
|
||||||
|
)
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|
@ -117,6 +127,7 @@ const Register = () => {
|
||||||
onSubmit={values => {
|
onSubmit={values => {
|
||||||
register({
|
register({
|
||||||
variables: {
|
variables: {
|
||||||
|
token: token,
|
||||||
username: username,
|
username: username,
|
||||||
password: values.password,
|
password: values.password,
|
||||||
role: role
|
role: role
|
||||||
|
|
|
||||||
|
|
@ -28,8 +28,13 @@ const VALIDATE_RESET_2FA_LINK = gql`
|
||||||
`
|
`
|
||||||
|
|
||||||
const RESET_2FA = gql`
|
const RESET_2FA = gql`
|
||||||
mutation reset2FA($userID: ID!, $secret: String!, $code: String!) {
|
mutation reset2FA(
|
||||||
reset2FA(userID: $userID, secret: $secret, code: $code)
|
$token: String!
|
||||||
|
$userID: ID!
|
||||||
|
$secret: String!
|
||||||
|
$code: String!
|
||||||
|
) {
|
||||||
|
reset2FA(token: $token, userID: $userID, secret: $secret, code: $code)
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|
@ -154,6 +159,7 @@ const Reset2FA = () => {
|
||||||
}
|
}
|
||||||
reset2FA({
|
reset2FA({
|
||||||
variables: {
|
variables: {
|
||||||
|
token: token,
|
||||||
userID: userID,
|
userID: userID,
|
||||||
secret: secret,
|
secret: secret,
|
||||||
code: twoFAConfirmation
|
code: twoFAConfirmation
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,8 @@ const VALIDATE_RESET_PASSWORD_LINK = gql`
|
||||||
`
|
`
|
||||||
|
|
||||||
const RESET_PASSWORD = gql`
|
const RESET_PASSWORD = gql`
|
||||||
mutation resetPassword($userID: ID!, $newPassword: String!) {
|
mutation resetPassword($token: String!, $userID: ID!, $newPassword: String!) {
|
||||||
resetPassword(userID: $userID, newPassword: $newPassword)
|
resetPassword(token: $token, userID: $userID, newPassword: $newPassword)
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|
@ -114,6 +114,7 @@ const ResetPassword = () => {
|
||||||
onSubmit={values => {
|
onSubmit={values => {
|
||||||
resetPassword({
|
resetPassword({
|
||||||
variables: {
|
variables: {
|
||||||
|
token: token,
|
||||||
userID: userID,
|
userID: userID,
|
||||||
newPassword: values.confirmPassword
|
newPassword: values.confirmPassword
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,9 +42,17 @@ const CHANGE_USER_ROLE = gql`
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const TOGGLE_USER_ENABLE = gql`
|
const ENABLE_USER = gql`
|
||||||
mutation toggleUserEnable($id: ID!) {
|
mutation enableUser($id: ID!) {
|
||||||
toggleUserEnable(id: $id) {
|
enableUser(id: $id) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const DISABLE_USER = gql`
|
||||||
|
mutation disableUser($id: ID!) {
|
||||||
|
disableUser(id: $id) {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -81,7 +89,11 @@ const Users = () => {
|
||||||
refetchQueries: () => ['users']
|
refetchQueries: () => ['users']
|
||||||
})
|
})
|
||||||
|
|
||||||
const [toggleUserEnable] = useMutation(TOGGLE_USER_ENABLE, {
|
const [enableUser] = useMutation(ENABLE_USER, {
|
||||||
|
refetchQueries: () => ['users']
|
||||||
|
})
|
||||||
|
|
||||||
|
const [disableUser] = useMutation(DISABLE_USER, {
|
||||||
refetchQueries: () => ['users']
|
refetchQueries: () => ['users']
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -307,7 +319,7 @@ const Users = () => {
|
||||||
showModal={showEnableUserModal}
|
showModal={showEnableUserModal}
|
||||||
toggleModal={toggleEnableUserModal}
|
toggleModal={toggleEnableUserModal}
|
||||||
user={userInfo}
|
user={userInfo}
|
||||||
confirm={toggleUserEnable}
|
confirm={userInfo?.enabled ? disableUser : enableUser}
|
||||||
inputConfirmToggle={toggleInputConfirmModal}
|
inputConfirmToggle={toggleInputConfirmModal}
|
||||||
setAction={setAction}
|
setAction={setAction}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,12 @@ const getClient = (history, location, setUserData) =>
|
||||||
onError(({ graphQLErrors, networkError }) => {
|
onError(({ graphQLErrors, networkError }) => {
|
||||||
if (graphQLErrors)
|
if (graphQLErrors)
|
||||||
graphQLErrors.forEach(({ message, locations, path, extensions }) => {
|
graphQLErrors.forEach(({ message, locations, path, extensions }) => {
|
||||||
handle(extensions?.code, history, location, setUserData)
|
handle(
|
||||||
|
{ message, locations, path, extensions },
|
||||||
|
history,
|
||||||
|
location,
|
||||||
|
setUserData
|
||||||
|
)
|
||||||
console.log(
|
console.log(
|
||||||
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
|
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
|
||||||
)
|
)
|
||||||
|
|
@ -47,21 +52,20 @@ const getClient = (history, location, setUserData) =>
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const handle = (type, ...args) => {
|
const handle = (apolloError, ...args) => {
|
||||||
const handler = {
|
const handler = {
|
||||||
UNAUTHENTICATED: ({ history, location, setUserData }) => {
|
UNAUTHENTICATED: (...args) => {
|
||||||
|
const history = args[0]
|
||||||
|
const location = args[1]
|
||||||
|
const setUserData = args[2]
|
||||||
setUserData(null)
|
setUserData(null)
|
||||||
if (location.pathname !== '/login') history.push('/login')
|
if (location.pathname !== '/login') history.push('/login')
|
||||||
},
|
}
|
||||||
INVALID_CREDENTIALS: () => {},
|
|
||||||
INVALID_TWO_FACTOR_CODE: () => {},
|
|
||||||
INVALID_URL_TOKEN: () => {},
|
|
||||||
USER_ALREADY_EXISTS: () => {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!R.has(type, handler)) throw new Error('Unknown error code.')
|
if (!R.has(apolloError.extensions?.code, handler)) return apolloError
|
||||||
|
|
||||||
return handler[type](...args)
|
return handler[apolloError.extensions?.code](...args)
|
||||||
}
|
}
|
||||||
|
|
||||||
const Provider = ({ children }) => {
|
const Provider = ({ children }) => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue