Browse Source

Add support for 2FA (#112)

master
Luke Childs 3 years ago
committed by GitHub
parent
commit
a42b1053f2
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      logic/auth.js
  2. 9
      logic/disk.js
  3. 19
      middlewares/auth.js
  4. 26
      modules/otp.js
  5. 2
      package.json
  6. 48
      routes/v1/account.js
  7. 10
      yarn.lock

2
logic/auth.js

@ -166,6 +166,8 @@ async function getInfo() {
//remove sensitive info //remove sensitive info
delete user.password; delete user.password;
delete user.seed; delete user.seed;
user.otpEnabled = Boolean(user.otpUri);
delete user.otpUri;
return user; return user;
} catch (error) { } catch (error) {

9
logic/disk.js

@ -73,6 +73,14 @@ async function writeUserFile(json) {
return diskService.writeJsonFile(constants.USER_FILE, json); return diskService.writeJsonFile(constants.USER_FILE, json);
} }
// Accept a function to apply a transformation to the user file
async function updateUserFile(update) {
const userData = await readUserFile();
const updatedUserData = update(userData);
return writeUserFile(updatedUserData);
}
async function writeUmbrelSeedFile(umbrelSeed) { async function writeUmbrelSeedFile(umbrelSeed) {
return diskService.ensureWriteFile(constants.UMBREL_SEED_FILE, umbrelSeed); return diskService.ensureWriteFile(constants.UMBREL_SEED_FILE, umbrelSeed);
} }
@ -299,6 +307,7 @@ module.exports = {
writeAppVersionFile, writeAppVersionFile,
writeSettingsFile, writeSettingsFile,
writeUserFile, writeUserFile,
updateUserFile,
writeUmbrelSeedFile, writeUmbrelSeedFile,
umbrelSeedFileExists, umbrelSeedFileExists,
settingsFileExists, settingsFileExists,

19
middlewares/auth.js

@ -7,6 +7,7 @@ const authLogic = require('logic/auth.js');
const NodeError = require('models/errors.js').NodeError; const NodeError = require('models/errors.js').NodeError;
const UUID = require('utils/UUID.js'); const UUID = require('utils/UUID.js');
const rsa = require('node-rsa'); const rsa = require('node-rsa');
const otp = require('modules/otp');
const JwtStrategy = passportJWT.Strategy; const JwtStrategy = passportJWT.Strategy;
const BasicStrategy = passportHTTP.BasicStrategy; const BasicStrategy = passportHTTP.BasicStrategy;
@ -85,10 +86,26 @@ function convertReqBodyToBasicAuth(req, res, next) {
function basic(req, res, next) { function basic(req, res, next) {
passport.authenticate(BASIC_AUTH, { session: false }, function (error, user) { passport.authenticate(BASIC_AUTH, { session: false }, function (error, user) {
function handleCompare(equal) { async function handleCompare(equal) {
if (!equal) { if (!equal) {
return next(new NodeError('Incorrect password', 401)); // eslint-disable-line no-magic-numbers return next(new NodeError('Incorrect password', 401)); // eslint-disable-line no-magic-numbers
} }
// Check if we have 2FA enabled
const userData = await diskLogic.readUserFile();
if (userData.otpUri) {
// Return an error if no OTP token is provided
if (!req.body.otpToken) {
return next(new NodeError('Missing OTP token', 401)); // eslint-disable-line no-magic-numbers
}
// Return an error if provided OTP token is invalid
if(!otp.verify(userData.otpUri, req.body.otpToken)) {
return next(new NodeError('Invalid OTP token', 401)); // eslint-disable-line no-magic-numbers
}
}
req.logIn(user, function (err) { req.logIn(user, function (err) {
if (err) { if (err) {
return next(new NodeError('Unable to authenticate', 401)); // eslint-disable-line no-magic-numbers return next(new NodeError('Unable to authenticate', 401)); // eslint-disable-line no-magic-numbers

26
modules/otp.js

@ -0,0 +1,26 @@
const crypto = require('crypto');
const {URL} = require('url');
const {totp} = require('notp');
const base32 = require('thirty-two');
const generateUri = () => {
const secret = crypto.randomBytes(32);
const encodedSecret = base32.encode(secret).toString('utf8').replace(/=/g,'');
const uri = `otpauth://totp/Umbrel?secret=${encodedSecret}&period=30&digits=6&algorithm=SHA1&issuer=getumbrel.com`;
return uri;
};
const verify = (uri, token) => {
const parsedUri = new URL(uri);
const secret = base32.decode(parsedUri.searchParams.get('secret'));
const period = parsedUri.searchParams.get('period');
const isValid = totp.verify(token, secret, {window: 6, time: period});
return Boolean(isValid);
};
module.exports = {
generateUri,
verify,
};

2
package.json

@ -28,6 +28,7 @@
"module-alias": "^2.1.0", "module-alias": "^2.1.0",
"morgan": "^1.9.0", "morgan": "^1.9.0",
"node-rsa": "^1.0.8", "node-rsa": "^1.0.8",
"notp": "^2.0.3",
"npm": "^6.14.6", "npm": "^6.14.6",
"passport": "^0.4.0", "passport": "^0.4.0",
"passport-http": "^0.3.0", "passport-http": "^0.3.0",
@ -35,6 +36,7 @@
"request-promise": "^4.2.2", "request-promise": "^4.2.2",
"semver": "^7.3.2", "semver": "^7.3.2",
"socks-proxy-agent": "^5.0.0", "socks-proxy-agent": "^5.0.0",
"thirty-two": "^1.0.2",
"uuid": "^8.0.0", "uuid": "^8.0.0",
"validator": "^13.0.0", "validator": "^13.0.0",
"winston": "^3.0.0-rc5", "winston": "^3.0.0-rc5",

48
routes/v1/account.js

@ -3,6 +3,7 @@ const router = express.Router();
// const applicationLogic = require('logic/application.js'); // const applicationLogic = require('logic/application.js');
const authLogic = require('logic/auth.js'); const authLogic = require('logic/auth.js');
const diskLogic = require('logic/disk.js');
const auth = require('middlewares/auth.js'); const auth = require('middlewares/auth.js');
const incorrectPasswordAuthHandler = require('middlewares/incorrectPasswordAuthHandler.js'); const incorrectPasswordAuthHandler = require('middlewares/incorrectPasswordAuthHandler.js');
@ -11,6 +12,8 @@ const constants = require('utils/const.js');
const safeHandler = require('utils/safeHandler'); const safeHandler = require('utils/safeHandler');
const validator = require('utils/validator.js'); const validator = require('utils/validator.js');
const otp = require('modules/otp');
const COMPLETE = 100; const COMPLETE = 100;
// Endpoint to change your password. // Endpoint to change your password.
@ -118,4 +121,49 @@ router.post('/refresh', auth.jwt, safeHandler((req, res) =>
.then(jwt => res.json(jwt)) .then(jwt => res.json(jwt))
)); ));
// Gets a new random OTP uri
router.get('/otpUri', auth.jwt, safeHandler(async (req, res) => {
const otpUri = otp.generateUri();
return res.status(constants.STATUS_CODES.OK).json(otpUri);
}));
// Enables OTP
router.post('/otp/enable', auth.jwt, safeHandler(async (req, res) => {
const {otpToken, otpUri} = req.body;
// Verify provided OTP token matched provided OTP uri
if(!otp.verify(otpUri, otpToken)) {
throw new Error('Invalid OTP Token');
}
// Insert otpUri into the user file
diskLogic.updateUserFile(userData => {
userData.otpUri = otpUri;
return userData;
});
return res.status(constants.STATUS_CODES.OK).json();
}));
// Disables OTP
router.post('/otp/disable', auth.jwt, safeHandler(async (req, res) => {
const {otpToken} = req.body;
// Read OTP uri on disk
const {otpUri} = await diskLogic.readUserFile();
// Verify provided OTP token
if(!otp.verify(otpUri, otpToken)) {
throw new Error('Invalid OTP Token');
}
// Remove OTP entry from user file
diskLogic.updateUserFile(userData => {
delete userData.otpUri;
return userData;
});
return res.status(constants.STATUS_CODES.OK).json();
}));
module.exports = router; module.exports = router;

10
yarn.lock

@ -3877,6 +3877,11 @@ normalize-url@^4.1.0:
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.0.tgz#453354087e6ca96957bd8f5baf753f5982142129" resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.0.tgz#453354087e6ca96957bd8f5baf753f5982142129"
integrity sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ== integrity sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==
notp@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/notp/-/notp-2.0.3.tgz#a9fd11e25cfe1ccb39fc6689544ee4c10ef9a577"
integrity sha1-qf0R4lz+HMs5/GaJVE7kwQ75pXc=
npm-audit-report@^1.3.2: npm-audit-report@^1.3.2:
version "1.3.3" version "1.3.3"
resolved "https://registry.yarnpkg.com/npm-audit-report/-/npm-audit-report-1.3.3.tgz#8226deeb253b55176ed147592a3995442f2179ed" resolved "https://registry.yarnpkg.com/npm-audit-report/-/npm-audit-report-1.3.3.tgz#8226deeb253b55176ed147592a3995442f2179ed"
@ -5709,6 +5714,11 @@ text-table@^0.2.0, text-table@~0.2.0:
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
thirty-two@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/thirty-two/-/thirty-two-1.0.2.tgz#4ca2fffc02a51290d2744b9e3f557693ca6b627a"
integrity sha1-TKL//AKlEpDSdEueP1V2k8prYno=
through2@^2.0.0: through2@^2.0.0:
version "2.0.5" version "2.0.5"
resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd"

Loading…
Cancel
Save