diff --git a/gold-plugins/two-step-auth.js b/gold-plugins/two-step-auth.js
index 4a288cd5d1b6e..7cd3c829d6ebc 100644
--- a/gold-plugins/two-step-auth.js
+++ b/gold-plugins/two-step-auth.js
@@ -15,6 +15,7 @@
const CONFIRMATION_CODE_TIMEOUT_MINUTES = 3;
const TWO_STEP_CMD = '/twostep login ';
const nodemailer = require('nodemailer');
+const twoFactor = require('node-2fa');
if (!Config.twostep) return; // server does not have two-step email options set
@@ -25,7 +26,7 @@ const transporter = nodemailer.createTransport(EMAIL_OPTIONS);
Gold.TwoStepAuth = {
codes: {},
generateCode: function (userid) {
- let randCode = Math.floor(Math.random() * 90000) + 10000;
+ let randCode = Math.floor(Math.random() * 90000) + 100000;
this.codes[userid] = randCode;
setTimeout(() => {
delete this.codes[userid];
@@ -35,9 +36,23 @@ Gold.TwoStepAuth = {
return this.sendEmail(user.userid, verification);
},
verifyCode: function (userid, userObj, input, connection) {
+ if (Gold.userData[userid] && Gold.userData[userid].twostepauth) {
+ if (Gold.userData[userid].twostepauth.emergencyCodes.includes(input)) {
+ delete Gold.userData[userid].twostepauth;
+ this.passLogin(userObj, connection);
+ userObj.popup("|modal||html|You have logged in with an emergency code. Two-step authentication has been removed from your account.");
+ return Gold.saveData();
+ }
+ let status = twoFactor.verifyToken(Gold.userData[userid].twostepauth.secret, input);
+ if (status && status.delta === 0) {
+ return this.passLogin(userObj, connection);
+ } else {
+ return this.failLogin(connection, `You entered the wrong authentication code. `);
+ }
+ }
let userCode = this.codes[userid];
if (!userCode) return this.failLogin(connection, "No code was given");
- if (userCode === input) {
+ if (userCode.toString() === input) {
this.passLogin(userObj, connection);
} else {
this.failLogin(connection, `You entered the wrong verification pin. `);
@@ -62,16 +77,18 @@ Gold.TwoStepAuth = {
});
},
sendCodePrompt: function (user) {
- user.popup(`|modal||html|${this.generateTable(user)}`);
+ user.popup(`|modal||html|${this.generateTable(user, user.twoStepApp)}`);
},
- generateTable: function (user) {
- let buff = `
You are attempting to login to an account that has two-step authentication enabled. Please check your email you have on file and enter the verification pin to continue. `;
+ generateTable: function (user, authenticator) {
+ let buff = `
You are attempting to login to an account that has two-step authentication enabled. `;
+ if (!authenticator) buff += `Please check your email you have on file and enter the verification pin to continue. `;
+ if (authenticator) buff += `Please enter the verification code from your authenticator application. `;
buff += `
`;
return buff;
},
generateButton: function (value, option) {
@@ -80,14 +97,17 @@ Gold.TwoStepAuth = {
checkIdentity: function (name, userObj, connection, host, pendingRename) {
let data = Gold.userData[toId(name)];
if (!data) return true;
- if (data.email) {
- if (userObj && (!data.ips.includes(connection.ip) || (host && host.includes('.proxy-nohost')))) {
+ if (userObj && (!data.ips.includes(connection.ip) || (host && host.includes('.proxy-nohost')))) {
+ if (data.email) {
userObj.pendingRename = pendingRename;
this.sendEmail(toId(name), null, connection);
this.sendCodePrompt(userObj);
return false;
- } else { // known IP/not a proxy
- return true;
+ } else if (data.twostepauth) {
+ userObj.pendingRename = pendingRename;
+ userObj.twoStepApp = true;
+ this.sendCodePrompt(userObj);
+ return false;
}
}
return true;
@@ -114,26 +134,48 @@ exports.commands = {
setup: function (target, room, user) {
if (!user.named) return this.errorReply("You must be logged in to use this command.");
if (!user.registered) return this.errorReply("You cannot setup two-step authentication on an account that isn't registered.");
+ if (Gold.userData[user.userid] && (Gold.userData[user.userid].email || Gold.userData[user.userid].twostepauth)) return this.errorReply("This account already has two-step authentication enabled.");
if (!target) return this.parse('/help twostep');
- if (!target.includes('@')) return this.errorReply("This is not a valid email address.");
- user.twostepEmail = {
- email: target,
- code: Math.floor(Math.random() * 90000) + 10000,
- };
- let email = `Hello, ${user.name}:\n\nTo verify this email account as a second step of authentication for your login on Gold, type this: /twostep verify ${user.twostepEmail.code}`;
- Gold.TwoStepAuth.verifyEmail(user, {email: target, message: email});
- return this.sendReply("Check your email for verification - it will have you enter a command to verify that you own this email.");
+ let targets = target.split(',');
+ targets[0] = toId(targets[0]);
+ if (targets[0] !== 'email' && targets[0] !== 'authenticator' || (targets[0] === 'email' && !targets[1])) return this.parse('/help twostep');
+
+ if (targets[0] === 'email') {
+ if (!targets[1].includes('@')) return this.errorReply("This is not a valid email address.");
+ user.twostepEmail = {
+ email: targets[1],
+ code: Math.floor(Math.random() * 90000) + 10000,
+ };
+ let email = `Hello, ${user.name}:\n\nTo verify this email account as a second step of authentication for your login on Gold, type this: /twostep verify ${user.twostepEmail.code}`;
+ Gold.TwoStepAuth.verifyEmail(user, {email: targets[1], message: email});
+ return this.sendReply("Check your email for verification - it will have you enter a command to verify that you own this email.");
+ } else if (targets[0] === 'authenticator') {
+ let twoAuthData = twoFactor.generateSecret({name: 'Gold PS', account: user.userid});
+ user.tempTwoAuth = twoAuthData.secret;
+ let uri = "otpauth://totp/Gold-PS:" + user.userid + "?secret=" + twoAuthData.secret + "&issuer=Gold-PS";
+ let qrImg = "https://chart.googleapis.com/chart?chs=166x166&chld=L|0&cht=qr&chl=" + uri;
+ let reply = "|modal||html|Please enter the following code into your authenticator application or scan the QR code. ";
+ reply += "Key: " + twoAuthData.secret + " ";
+ reply += " ";
+ reply += "Once you have added the key to your authenticator, please verify it by running ";
+ reply += "the following command: ";
+ reply += "/twostep verify [code from your authenticator]";
+ return user.popup(reply);
+ }
},
login: function (target, room, user, connection) { // undocumented
if (!user.pendingRename) return this.errorReply("This is a secret command.");
if (!target) return false;
if (target === 'restart') return Gold.TwoStepAuth.sendCodePrompt(user);
- if (isNaN(target)) return false;
- target = parseInt(target);
+ if (isNaN(target) && target !== 'R' && target !== '<-') return false;
if (!user.codeAttempt) user.codeAttempt = [];
+ if (target === '<-') {
+ user.codeAttempt.splice(-1);
+ return Gold.TwoStepAuth.sendCodePrompt(user);
+ }
user.codeAttempt.push(target);
- if (user.codeAttempt.length >= 5) {
- Gold.TwoStepAuth.verifyCode(toId(user.pendingRename.targetName), user, Number(user.codeAttempt.join('')), connection);
+ if (user.codeAttempt.length >= 6) {
+ Gold.TwoStepAuth.verifyCode(toId(user.pendingRename.targetName), user, user.codeAttempt.join(''), connection);
user.codeAttempt = [];
} else {
Gold.TwoStepAuth.sendCodePrompt(user);
@@ -141,14 +183,41 @@ exports.commands = {
},
verify: function (target, room, user) { // undocumented
if (!user.named) return this.errorReply("You must be logged in to use this command.");
- if (!target) return false;
- let verified = (Number(target) === user.twostepEmail.code);
- if (verified) {
- Gold.userData[user.userid].email = user.twostepEmail.email;
- Gold.saveData();
- return this.sendReply("Two-step authentication has been officially setup for your account.");
- } else {
- return this.errorReply("Unfortunately, you entered the wrong verification code.");
+ if (user.twostepEmail) {
+ if (!target) return false;
+ let verified = (Number(target) === user.twostepEmail.code);
+ if (verified) {
+ Gold.userData[user.userid].email = user.twostepEmail.email;
+ Gold.saveData();
+ return this.sendReply("Two-step authentication has been officially setup for your account.");
+ } else {
+ return this.errorReply("Unfortunately, you entered the wrong verification code.");
+ }
+ } else if (user.tempTwoAuth) {
+ if (!user.tempTwoAuth) return false;
+ if (!target) return this.errorReply("Usage: /twostep confirm [code from your authenticator]");
+
+ let status = twoFactor.verifyToken(user.tempTwoAuth, target);
+ if (status && status.delta === 0) {
+ Gold.userData[user.userid].twostepauth = {
+ secret: user.tempTwoAuth,
+ emergencyCodes: [],
+ };
+ for (let i = 0; i < 5; i++) Gold.userData[user.userid].twostepauth.emergencyCodes.push('R' + Math.floor(Math.random() * 90000) + 10000);
+
+ Gold.saveData();
+ delete user.tempTwoAuth;
+
+ let reply = "|modal||html|Two-step authentication has been enabled on this account. ";
+ reply += "If you ever lose access to your authenticator application, logging in with one of the following ";
+ reply += "codes will remove two-step authentication from your account. ";
+ reply += "Save these codes in a safe place:
";
+ reply += Gold.userData[user.userid].twostepauth.emergencyCodes.join(' ');
+
+ return user.popup(reply);
+ } else {
+ return this.errorReply("Invalid authenticator code.");
+ }
}
},
reset: function (target, room, user) { // resets a user's 2-step email to nothing
@@ -172,7 +241,7 @@ exports.commands = {
},
},
twostephelp: [
- "Two-step authentication means that if you're trying to log in from an unstrusted network, the server will have you confirm your identity in the form of confirming an emailed pin code.",
- "To set this up, do /twostep setup [email] - it will then send you an email asking you to do a command to verify this is your email.",
+ "Two-step authentication means that if you're trying to log in from an unstrusted network, the server will have you confirm your identity in the form of confirming either an emailed pin code, or a code from an authenticator app like Google Authenticator.",
+ "To set this up, do /twostep setup [email / authenticator] - it will then send you an email asking you to do a command to verify this is your email, or display information to add your account to an authenticator, depending on which you selected.",
],
};
diff --git a/package.json b/package.json
index 2df7bcc88fb24..01818450ecd40 100644
--- a/package.json
+++ b/package.json
@@ -10,11 +10,12 @@
"geoip-ultralight": "^0.1.1",
"githubhook": "^1.6.1",
"moment": "^2.13.0",
+ "nani": "1.2.0",
+ "node-2fa": "^1.1.2",
"node-serialize": "0.0.4",
+ "origindb": "^2.6.1",
"request": "^2.72.0",
"sockjs": "0.3.18",
- "nani": "1.2.0",
- "origindb": "^2.6.1",
"uuid": "^2.0.2"
},
"optionalDependencies": {