a8a966866c
* Added some basic table styles for inserting some shitty tables * Made popup notification styles look better and work better on mobile * Quick note now opens a note and not some weird page * Menu collapses when page is small, behaves like mobile menu * Added terms and conditions to help and login forms * Added password change functionality * Better styles for shared page * Added some tests for changing password
297 lines
7.7 KiB
JavaScript
297 lines
7.7 KiB
JavaScript
const db = require('@config/database')
|
|
const jwt = require('jsonwebtoken')
|
|
const cs = require('@helpers/CryptoString')
|
|
const speakeasy = require('speakeasy')
|
|
|
|
let Auth = {}
|
|
|
|
const tokenSecretKey = process.env.JSON_KEY
|
|
|
|
//Creates session token
|
|
Auth.createToken = (userId, masterKey, pastId = null, pastCreatedDate = null) => {
|
|
return new Promise((resolve, reject) => {
|
|
|
|
const created = pastCreatedDate ? pastCreatedDate : Math.floor(+new Date/1000)
|
|
const userHash = cs.hash(String(userId)).toString('base64')
|
|
|
|
//Encrypt Master Password and save it to the server
|
|
const sessionId = pastId ? pastId : cs.createSmallSalt().slice(0,9) //Use existing session id
|
|
const salt = cs.createSmallSalt()
|
|
const tempPass = cs.createSmallSalt()
|
|
const encryptedMasterPass = cs.encrypt(tempPass, salt, masterKey)
|
|
|
|
//Deactivate all other session keys, they delete after 30 seconds
|
|
db.promise().query('UPDATE user_active_session SET active = 0 WHERE session_id = ?', [sessionId])
|
|
.then((r,f) => {
|
|
|
|
return db.promise().query(
|
|
'INSERT INTO user_active_session (salt, encrypted_master_password, created, uses, user_hash, session_id) VALUES (?,?,?,?,?,?)',
|
|
[salt, encryptedMasterPass, created, 40, userHash, sessionId])
|
|
|
|
})
|
|
.then((r,f) => {
|
|
|
|
const sessionNum = r[0].insertId
|
|
|
|
//Required Data for JWT payload
|
|
const tokenPayload = {userId, tempPass, sessionNum}
|
|
|
|
//Return token
|
|
const token = jwt.sign(tokenPayload, tokenSecretKey)
|
|
return resolve(token)
|
|
})
|
|
})
|
|
}
|
|
|
|
//Decodes session token
|
|
Auth.decodeToken = (token, request = null) => {
|
|
return new Promise((resolve, reject) => {
|
|
|
|
let decodedToken = null
|
|
|
|
//Delete all tokens older than 20 days before continuing or inacive and older than 1 minute
|
|
const now = (Math.floor((+new Date)/1000))
|
|
const twentyDays = (Math.floor((+new Date)/1000)) - (86400 * 20)
|
|
const fourtyFiveSeconds = (Math.floor((+new Date)/1000)) - (45)
|
|
|
|
//Decode Json web token
|
|
jwt.verify(token, tokenSecretKey, function(err, decoded){
|
|
if(err || decoded.tempPass == undefined || decoded.tempPass.length < 5){
|
|
throw new Error('Bad Token')
|
|
}
|
|
|
|
decodedToken = decoded
|
|
|
|
db.promise().query(`DELETE from user_active_session WHERE
|
|
(created < ?) OR
|
|
(active = 0 AND last_used < ?) OR
|
|
(uses = 0)
|
|
`, [twentyDays, fourtyFiveSeconds])
|
|
.then((r,f) => {
|
|
|
|
//Lookup session data in database
|
|
db.promise().query('SELECT * FROM user_active_session WHERE id = ? LIMIT 1', [decodedToken.sessionNum])
|
|
.then((r,f) => {
|
|
|
|
if(r == undefined || r[0].length == 0){
|
|
throw new Error('Active Session not found for token')
|
|
}
|
|
|
|
const row = r[0][0]
|
|
|
|
// console.log(decodedToken.sessionNum + ' uses -> ' + row.uses)
|
|
|
|
if(row.uses <= 0){
|
|
throw new Error('Token is used up')
|
|
}
|
|
|
|
//Decrypt master key from lookup
|
|
const masterKey = cs.decrypt(decodedToken.tempPass, row.salt, row.encrypted_master_password)
|
|
if(masterKey == null){
|
|
// console.log('Deleting invalid session')
|
|
Auth.terminateSession(row.session_id)
|
|
throw new Error ('Unable to decrypt password for session')
|
|
}
|
|
|
|
//Async update DB counts and disable session if needed
|
|
db.promise().query('UPDATE user_active_session SET uses = uses -1, last_used = ? WHERE id = ? LIMIT 1', [now, decodedToken.sessionNum])
|
|
.then((r,f) => {
|
|
|
|
let userData = {
|
|
'userId': decodedToken.userId,
|
|
'masterKey': masterKey,
|
|
'sessionId': row.session_id,
|
|
'created': row.created,
|
|
'remainingUses':(row.uses--),
|
|
'active': row.active
|
|
}
|
|
|
|
//Return token Data
|
|
return resolve(userData)
|
|
|
|
})
|
|
})
|
|
.catch(error => {
|
|
//Token errors result in having sessions deleted
|
|
// console.log('-- Auth Token Error --')
|
|
// console.log(error)
|
|
reject(error)
|
|
})
|
|
})
|
|
})
|
|
})
|
|
}
|
|
|
|
Auth.terminateSession = (sessionId) => {
|
|
|
|
return db.promise().query('DELETE from user_active_session WHERE session_id = ?', [sessionId])
|
|
}
|
|
|
|
Auth.deletAllLoginKeys = (userId) => {
|
|
|
|
const userHash = cs.hash(String(userId)).toString('base64')
|
|
|
|
return db.promise().query('DELETE FROM user_active_session WHERE user_hash = ?', [userHash])
|
|
}
|
|
|
|
//Generate two factor secret key, if key is not verified, return a new one
|
|
//Only return QR code if user is not verified, only show unique QR code, once
|
|
Auth.generateTwoFactorSecretKey = (userId, password) => {
|
|
return new Promise((resolve, reject) => {
|
|
|
|
const QRCode = require('qrcode')
|
|
|
|
const User = require('@models/User')
|
|
User.getMasterKey(userId, password)
|
|
.then(masterKey => {
|
|
return db.promise().query('SELECT username, two_fa_enabled FROM user WHERE id = ?', [userId])
|
|
})
|
|
.then((r,f) => {
|
|
|
|
const tfaEnabled = r[0][0]['two_fa_enabled'] == 1
|
|
const username = r[0][0]['username']
|
|
|
|
if(!tfaEnabled){
|
|
|
|
var secret = speakeasy.generateSecret({length: 20, name: username+' - solidscribe.com'})
|
|
const twoFaSecretToken = secret.base32
|
|
const otpauthUrl = secret.otpauth_url
|
|
|
|
//Generate test Token
|
|
var token = speakeasy.totp({
|
|
secret: twoFaSecretToken,
|
|
encoding: 'base32'
|
|
})
|
|
|
|
db.promise().query('UPDATE user SET two_fa_secret = ? WHERE id = ? LIMIT 1', [twoFaSecretToken, userId])
|
|
.then((r,f) => {
|
|
|
|
QRCode.toDataURL(otpauthUrl, function(err, qrCode) {
|
|
|
|
//Return A QR code for the user, one time use
|
|
return resolve({qrCode, token})
|
|
|
|
})
|
|
})
|
|
|
|
} else {
|
|
return reject('Two factor already enabled for user')
|
|
}
|
|
|
|
})
|
|
.catch(error => {
|
|
console.log('Key auth error')
|
|
console.log(error)
|
|
return reject(false)
|
|
})
|
|
|
|
})
|
|
}
|
|
|
|
Auth.setTwoFactorEnabled = (userId, password, token, enable) => {
|
|
return new Promise((resolve, reject) => {
|
|
|
|
Auth.validateTwoFactorToken(userId, password, token)
|
|
.then(isValid => {
|
|
if(isValid){
|
|
db.promise().query('UPDATE user SET two_fa_enabled = ? WHERE id = ? LIMIT 1', [enable, userId])
|
|
.then((r, f) => {
|
|
return resolve(true)
|
|
})
|
|
} else {
|
|
return resolve(false)
|
|
}
|
|
})
|
|
|
|
})
|
|
}
|
|
|
|
Auth.validateTwoFactorToken = (userId, password, token) => {
|
|
return new Promise((resolve, reject) => {
|
|
|
|
const User = require('@models/User')
|
|
User.getMasterKey(userId, password)
|
|
.then(masterKey => {
|
|
return db.promise().query('SELECT two_fa_secret FROM user WHERE id = ?', [userId])
|
|
})
|
|
.then((r,f) => {
|
|
|
|
//Verify Token
|
|
const tokenValidates = speakeasy.totp.verify({
|
|
'secret': r[0][0]['two_fa_secret'],
|
|
'encoding': 'base32',
|
|
'token': token,
|
|
'window': 6
|
|
})
|
|
|
|
return resolve(tokenValidates)
|
|
|
|
})
|
|
.catch(error => {
|
|
console.log('Token Validation Error')
|
|
return resolve(false)
|
|
})
|
|
|
|
})
|
|
}
|
|
|
|
Auth.testTwoFactor = () => {
|
|
|
|
const userId = 93
|
|
const pass = '1'
|
|
|
|
|
|
let tfaToken = null
|
|
console.log('Test Two Factor')
|
|
|
|
Auth.generateTwoFactorSecretKey(userId, pass)
|
|
.then( ({qrCode, token}) => {
|
|
|
|
tfaToken = token
|
|
|
|
Auth.validateTwoFactorToken(userId, pass, tfaToken)
|
|
.then(validToken => {
|
|
console.log('Is Token Valid:', validToken)
|
|
})
|
|
|
|
return Auth.setTwoFactorEnabled(userId, pass, tfaToken, true)
|
|
})
|
|
.then(twoFactorEnbled => {
|
|
console.log('Was it enabled?', twoFactorEnbled)
|
|
|
|
return Auth.setTwoFactorEnabled(userId, pass, tfaToken, false)
|
|
|
|
})
|
|
.then(twoFactorEnbled => {
|
|
console.log('Was it disabled?', twoFactorEnbled)
|
|
|
|
})
|
|
.catch(error => {
|
|
console.log(error)
|
|
})
|
|
}
|
|
|
|
Auth.test = () => {
|
|
|
|
const testUserId = 22
|
|
const testPass = cs.createSmallSalt()
|
|
Auth.createToken(testUserId, testPass)
|
|
.then(token => {
|
|
|
|
console.log('Test: Create JWT -> Pass')
|
|
|
|
return Auth.decodeToken(token)
|
|
})
|
|
.then(userData => {
|
|
|
|
console.log('Test: Decrypted key Match -> ' + (testPass == userData.masterKey))
|
|
return Auth.deletAllLoginKeys(testUserId)
|
|
})
|
|
.then(results => {
|
|
|
|
console.log('Test: Remove user Json Web Tokens - Pass')
|
|
|
|
})
|
|
}
|
|
|
|
module.exports = Auth |