-
CONNECT
@@ -47,7 +47,7 @@ module.exports = {
global: shared.state
}),
mounted: function() {
- const authDash = { name: 'authenticate', query: { forwardTo: '/dashboard' }};
+ const authDash = { name: 'loginTag', query: { forwardTo: '/dashboard' }};
if (this.global.onboardStatus === 'complete') {
this.$router.push(authDash);
return;
diff --git a/package.json b/package.json
index 39b1097..cd1bc28 100644
--- a/package.json
+++ b/package.json
@@ -25,7 +25,6 @@
},
"dependencies": {
"adm-zip": "^0.4.7",
- "bcrypt": "^1.0.3",
"browserify": "^14.5.0",
"csv-stringify": "2.0.0",
"envify": "^4.1.0",
diff --git a/server/atlas_client.js b/server/atlas_client.js
index 10c70ce..adce530 100644
--- a/server/atlas_client.js
+++ b/server/atlas_client.js
@@ -20,6 +20,7 @@ class BotAtlasClient extends relay.AtlasClient {
);
const creator = `@${botUser.tag.slug}:${botUser.org.slug}`;
console.info(`Bot onboarding performed by: ${creator}`);
+ await relay.storage.set('authentication', 'adminIds', [botUser.id]);
await relay.storage.putState("onboardUser", botUser.id);
if (this.onboardingCreatedUser) {
try {
diff --git a/server/forsta_bot.js b/server/forsta_bot.js
index 458c726..32edb01 100644
--- a/server/forsta_bot.js
+++ b/server/forsta_bot.js
@@ -8,6 +8,10 @@ const relay = require("librelay");
const PGStore = require("./pgstore");
const uuid4 = require("uuid/v4");
+async function sleep(ms) {
+ return await new Promise(resolve => setTimeout(resolve, ms));
+}
+
class ForstaBot {
async start() {
const ourId = await relay.storage.getState('addr');
@@ -15,6 +19,7 @@ class ForstaBot {
console.warn('bot is not yet registered');
return;
}
+ await this.migrate();
console.info('Starting message receiver for:', ourId);
this.pgStore = new PGStore('message_vault');
await this.pgStore.initialize();
@@ -41,6 +46,17 @@ class ForstaBot {
await this.msgReceiver.connect();
}
+ async migrate() {
+ const adminIds = await relay.storage.get('authentication', 'adminIds');
+ if (!adminIds) {
+ // need to convert from a password-auth sort of bot to the new auth-code system
+ const onboardUser = await relay.storage.getState("onboardUser");
+ if (!onboardUser) return; // should never happen, but just in case..
+ await relay.storage.set('authentication', 'adminIds', [onboardUser]);
+ await relay.storage.remove('authentication', 'pwhash');
+ }
+ }
+
async stop() {
if (this.msgReceiver) {
console.warn('Stopping message receiver');
@@ -70,6 +86,7 @@ class ForstaBot {
}
fqLabel(user) { return `${this.fqTag(user)} (${this.fqName(user)})`; }
+
async onMessage(ev) {
const received = new Date(ev.data.timestamp);
const envelope = JSON.parse(ev.data.message.body);
@@ -127,6 +144,161 @@ class ForstaBot {
});
}
}
+
+ async getSoloAuthThreadId() {
+ let id = await relay.storage.get('authentication', 'soloThreadId');
+ if (!id) {
+ id = uuid4();
+ relay.storage.set('authentication', 'soloThreadId', id);
+ }
+
+ return id;
+ }
+
+ async getGroupAuthThreadId() {
+ let id = await relay.storage.get('authentication', 'groupThreadId');
+ if (!id) {
+ id = uuid4();
+ relay.storage.set('authentication', 'groupThreadId', id);
+ }
+
+ return id;
+ }
+
+ genAuthCode(expirationMinutes) {
+ const code = ('000000' + Math.floor(Math.random() * 1000000)).slice(-6);
+ const expires = new Date();
+ expires.setMinutes(expires.getMinutes() + expirationMinutes);
+ return { code, expires };
+ }
+
+ removeExpiredAuthCodes(pending) {
+ const now = new Date();
+
+ Object.keys(pending).forEach(uid => {
+ pending[uid].expires = new Date(pending[uid].expires); // todo: fix store encoding...
+ if (pending[uid].expires < now) {
+ delete pending[uid];
+ }
+ });
+
+ return pending;
+ }
+
+ async sendAuthCode(tag) {
+ tag = (tag && tag[0] === '@') ? tag : '@' + tag;
+ const resolved = await this.resolveTags(tag);
+ if (resolved.userids.length === 1 && resolved.warnings.length === 0) {
+ const uid = resolved.userids[0];
+ const adminIds = await relay.storage.get('authentication', 'adminIds');
+ if (!adminIds.includes(uid)) {
+ throw { statusCode: 403, info: { tag: ['not an approved administrator'] } };
+ }
+
+ const auth = this.genAuthCode(1);
+ this.msgSender.send({
+ distribution: resolved,
+ threadId: await this.getGroupAuthThreadId(),
+ text: `${auth.code} is your authentication code, valid for one minute`
+ });
+ const pending = await relay.storage.get('authentication', 'pending', {});
+ pending[uid] = auth;
+ await relay.storage.set('authentication', 'pending', pending);
+
+ return resolved.userids[0];
+ } else {
+ throw { statusCode: 400, info: { tag: ['not a recognized tag, please try again'] } };
+ }
+ }
+
+ async validateAuthCode(userId, code) {
+ let pending = await relay.storage.get('authentication', 'pending', {});
+ pending = this.removeExpiredAuthCodes(pending);
+ const auth = pending[userId];
+ if (!auth) {
+ throw { statusCode: 403, info: { code: ['no authentication pending, please start over'] } };
+ }
+ if (auth.code != code) {
+ await sleep(500); // throttle guessers
+ throw { statusCode: 403, info: { code: ['incorrect code, please try again'] } };
+ }
+
+ delete pending[userId];
+ relay.storage.set('authentication', 'pending', pending);
+
+ await this.broadcastNotice('has successfully authenticated as an administrator', userId);
+ return true;
+ }
+
+ async getAdministrators() {
+ const adminIds = await relay.storage.get('authentication', 'adminIds', []);
+ const adminUsers = await this.getUsers(adminIds);
+ const admins = adminUsers.map(u => {
+ return {
+ id: u.id,
+ label: this.fqLabel(u)
+ };
+ });
+ return admins;
+ }
+
+ async broadcastNotice(action, actorUserId) {
+ const adminIds = await relay.storage.get('authentication', 'adminIds', []);
+ let added = false;
+ if (!adminIds.includes(actorUserId)) {
+ adminIds.push(actorUserId);
+ added = true;
+ }
+ const adminUsers = await this.getUsers(adminIds);
+ const actor = adminUsers.find(u => u.id === actorUserId);
+ const actorLabel = actor ? this.fqLabel(actor) : '';
+ const expression = adminUsers.map(u => this.fqTag(u)).join(' + ');
+ const distribution = await this.resolveTags(expression);
+
+ const adminList = adminUsers.filter(u => !(added && u.id === actorUserId)).map(u => this.fqLabel(u)).join('\n');
+
+ const fullMessage = `Note: ${actorLabel} ${action}.\n\nCurrent administrators are:\n${adminList}`;
+ const subbedFullMessage = fullMessage.replace(/<<([^>]*)>>/g, (_, id) => {
+ const user = adminUsers.find(x => x.id === id);
+ return this.fqLabel(user);
+ });
+
+ this.msgSender.send({
+ distribution,
+ threadId: await this.getSoloAuthThreadId(),
+ text: subbedFullMessage
+ });
+ }
+
+ async addAdministrator({addTag, actorUserId}) {
+ const tag = (addTag && addTag[0] === '@') ? addTag : '@' + addTag;
+ const resolved = await this.resolveTags(tag);
+ if (resolved.userids.length === 1 && resolved.warnings.length === 0) {
+ const uid = resolved.userids[0];
+ const adminIds = await relay.storage.get('authentication', 'adminIds');
+ if (!adminIds.includes(uid)) {
+ adminIds.push(uid);
+ await relay.storage.set('authentication', 'adminIds', adminIds);
+ }
+ await this.broadcastNotice(`has added <<${uid}>> to the administrator list`, actorUserId);
+ return this.getAdministrators();
+ }
+ throw { statusCode: 400, info: { tag: ['not a recognized tag, please try again'] } };
+ }
+
+ async removeAdministrator({removeId, actorUserId}) {
+ const adminIds = await relay.storage.get('authentication', 'adminIds', []);
+ const idx = adminIds.indexOf(removeId);
+
+ if (idx < 0) {
+ throw { statusCode: 400, info: { id: ['administrator id not found'] } };
+ }
+ adminIds.splice(idx, 1);
+ await this.broadcastNotice(`is removing <<${removeId}>> from the administrator list`, actorUserId);
+ await relay.storage.set('authentication', 'adminIds', adminIds);
+
+ return this.getAdministrators();
+ }
}
module.exports = ForstaBot;
diff --git a/server/web/api.js b/server/web/api.js
index 6eebe66..284488e 100644
--- a/server/web/api.js
+++ b/server/web/api.js
@@ -3,11 +3,8 @@ const csvStringify = require('csv-stringify');
const express = require('express');
const relay = require('librelay');
const jwt = require('jsonwebtoken');
-const bcrypt = require('bcrypt');
const uuidv4 = require('uuid/v4');
-const bcryptSaltRounds = 12;
-
class APIHandler {
@@ -30,8 +27,8 @@ class APIHandler {
relay.storage.get('authentication', 'jwtsecret')
.then((secret) => {
try {
- jwt.verify(parts[1], secret);
- fn.call(this, req, res, next).catch(e => {
+ const jwtInfo = jwt.verify(parts[1], secret);
+ fn.call(this, req, res, next, jwtInfo.userId).catch(e => {
console.error('Async Route Error:', e);
next();
});
@@ -159,73 +156,86 @@ class AuthenticationAPIV1 extends APIHandler {
constructor(options) {
super(options);
- this.router.get('/status/v1', this.asyncRoute(this.onGetStatus, false));
- this.router.post('/login/v1', this.asyncRoute(this.onLogin, false));
- this.router.post('/password/v1', this.asyncRoute(this.onPost, false));
- this.router.put('/password/v1', this.asyncRoute(this.onPut));
+ this.router.get('/login/v1/:tag', this.asyncRoute(this.onRequestLoginCode, false));
+ this.router.post('/login/v1', this.asyncRoute(this.onValidateLoginCode, false));
+ this.router.get('/admins/v1', this.asyncRoute(this.onGetAdministrators));
+ this.router.post('/admins/v1', this.asyncRoute(this.onUpdateAdministrators));
}
- async genToken() {
+ async genToken(userId) {
let secret = await relay.storage.get('authentication', 'jwtsecret');
if (!secret) {
secret = uuidv4();
await relay.storage.set('authentication', 'jwtsecret', secret);
}
- return jwt.sign({}, secret, { algorithm: "HS512", expiresIn: 2*60*60 /* later: "2 days" */ });
+ return jwt.sign({ userId }, secret, { algorithm: "HS512", expiresIn: 2*60*60 /* later: "2 days" */ });
}
- async passwordHash(hash) {
- if (hash) {
- return await relay.storage.set('authentication', 'pwhash', hash);
- } else {
- return await relay.storage.get('authentication', 'pwhash');
+ async onRequestLoginCode(req, res) {
+ const tag = req.params.tag;
+ if (!tag) {
+ res.status(412).json({
+ error: 'missing_arg',
+ message: 'Missing URL param: tag'
+ });
+ return;
}
- }
-
- async onGetStatus(req, res) {
- const stashedHash = await this.passwordHash();
- if (stashedHash) {
- res.status(204).json({ });
- } else {
- res.status(404).json({error: 'not_configured'});
+ try {
+ const id = await this.server.bot.sendAuthCode(tag);
+ res.status(200).json({ id });
+ return;
+ } catch (e) {
+ res.status(e.statusCode || 500).json(e.info || { message: 'internal error'});
+ return;
}
}
- async onLogin(req, res) {
- const password = req.body.password;
- const stashedHash = await this.passwordHash();
- if (!stashedHash || bcrypt.compareSync(password, stashedHash)) {
- // yes, if there is no stashed password hash, we give them a session
- const token = await this.genToken();
+ async onValidateLoginCode(req, res) {
+ const userId = req.body.id;
+ const code = req.body.code;
+
+ try {
+ await this.server.bot.validateAuthCode(userId, code);
+ const token = await this.genToken(userId);
res.status(200).json({ token });
- } else {
- res.status(401).json({ password: 'incorrect password' });
+ return;
+ } catch (e) {
+ res.status(e.statusCode || 500).json(e.info || { message: 'internal error'});
+ return;
}
}
- async onPost(req, res) {
- const exists = !!await this.passwordHash();
- if (!exists) {
- const password = req.body.password;
- const hash = await bcrypt.hash(password, bcryptSaltRounds);
- this.passwordHash(hash);
- const token = await this.genToken();
- res.status(201).json({ token });
- } else {
- res.status(405).json({ password: 'already exists' });
+ async onGetAdministrators(req, res) {
+ try {
+ const admins = await this.server.bot.getAdministrators();
+ res.status(200).json({ administrators: admins });
+ return;
+ } catch (e) {
+ console.log('problem in get administrators', e);
+ res.status(e.statusCode || 500).json(e.info || { message: 'internal error'});
+ return;
}
}
- async onPut(req, res) {
- const exists = !!this.passwordHash();
- if (exists) {
- const password = req.body.password;
- const hash = await bcrypt.hash(password, bcryptSaltRounds);
- this.passwordHash(hash);
- const token = await this.genToken();
- res.status(201).json({ token });
- } else {
- res.status(405).json({ password: 'does not exist' });
+ async onUpdateAdministrators(req, res, next, userId) {
+ const op = req.body.op;
+ const id = req.body.id;
+ const tag = req.body.tag;
+
+ if (!(op === 'add' && tag || op === 'remove' && id)) {
+ res.status(400).json({ non_field_errors: ['must either add tag or remove id'] });
+ }
+
+ try {
+ const admins = (op === 'add')
+ ? await this.server.bot.addAdministrator({addTag: tag, actorUserId: userId})
+ : await this.server.bot.removeAdministrator({removeId: id, actorUserId: userId});
+ res.status(200).json({ administrators: admins });
+ return;
+ } catch (e) {
+ console.log('problem in update administrators', e);
+ res.status(e.statusCode || 500).json(e.info || { message: 'internal error'});
+ return;
}
}
}