Skip to content

Commit

Permalink
feat(api): update otp library
Browse files Browse the repository at this point in the history
  • Loading branch information
sws2apps-admin authored Dec 4, 2022
1 parent 93dd853 commit e9ce7d3
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 52 deletions.
37 changes: 35 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"node-fetch": "^3.3.0",
"nodemailer": "^6.8.0",
"nodemailer-express-handlebars": "^5.0.0",
"otpauth": "^9.0.2",
"randomstring": "^1.2.3",
"request-ip": "^3.3.0",
"serve-favicon": "^2.5.0",
Expand Down
45 changes: 36 additions & 9 deletions src/classes/User.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { getAuth } from "firebase-admin/auth";
import { FieldValue, getFirestore } from "firebase-admin/firestore";
import twofactor from "node-2fa";
import * as OTPAuth from "otpauth";
import randomstring from "randomstring";
import { decryptData, encryptData } from "../utils/encryption-utils.js";
import { sendUserResetPassword } from "../utils/sendEmail.js";
import { Congregations } from "./Congregations.js";
import { Users } from "./Users.js";

const db = getFirestore(); //get default database

Expand Down Expand Up @@ -184,13 +185,25 @@ export class User {
this.sessions = [];
};

generateSecret = async (email) => {
generateSecret = async () => {
try {
const secret = twofactor.generateSecret({
name: "sws2apps",
account: email,
const tempSecret = new OTPAuth.Secret().base32;

const totp = new OTPAuth.TOTP({
issuer: "sws2apps",
label: this.user_uid,
algorithm: "SHA1",
digits: 6,
period: 30,
secret: OTPAuth.Secret.fromBase32(tempSecret),
});

const secret = {
secret: tempSecret,
uri: totp.toString(),
version: 2,
};

const encryptedData = encryptData(secret);

// save secret
Expand Down Expand Up @@ -223,7 +236,8 @@ export class User {
decryptSecret = () => {
try {
const decryptedData = decryptData(this.secret);
return JSON.parse(decryptedData);
const secret = JSON.parse(decryptedData);
return { ...secret, version: secret.version || 1 };
} catch (error) {
throw new Error(error.message);
}
Expand Down Expand Up @@ -300,11 +314,23 @@ export class User {

revokeToken = async () => {
// generate new secret and encrypt
const secret = twofactor.generateSecret({
name: "sws2apps",
account: this.user_uid,
const tempSecret = new OTPAuth.Secret().base32;

const totp = new OTPAuth.TOTP({
issuer: "sws2apps",
label: this.user_uid,
algorithm: "SHA1",
digits: 6,
period: 30,
secret: OTPAuth.Secret.fromBase32(tempSecret),
});

const secret = {
secret: tempSecret,
uri: totp.toString(),
version: 2,
};

const encryptedData = encryptData(secret);

// remove all sessions and save new secret
Expand Down Expand Up @@ -361,6 +387,7 @@ export class User {

this.sessions = newSessions;

await Users.loadAll();
await Congregations.loadAll();
};
}
3 changes: 2 additions & 1 deletion src/controllers/auth-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,14 @@ export const loginUser = async (req, res, next) => {

res.status(200).json({ message: "MFA_VERIFY" });
} else {
const secret = await user.generateSecret(email);
const secret = await user.generateSecret();

res.locals.type = "warn";
res.locals.message = "user authentication rejected because account mfa is not yet setup";
res.status(403).json({
secret: secret.secret,
qrCode: secret.uri,
version: secret.version,
});
}
} else {
Expand Down
116 changes: 76 additions & 40 deletions src/controllers/mfa-controller.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// dependencies
import twofactor from "node-2fa";
import * as OTPAuth from "otpauth";
import { validationResult } from "express-validator";
import { Users } from "../classes/Users.js";

Expand Down Expand Up @@ -28,50 +28,86 @@ export const verifyToken = async (req, res, next) => {

try {
const user = Users.findUserById(id);
const { secret } = user.decryptSecret();

// 2fa verification
const verified = twofactor.verifyToken(secret, token);

if (verified?.delta === 0) {
// update mfa enabled && verified
const { visitorid } = req.headers;

let newSessions = sessions.map((session) => {
if (session.visitorid === visitorid) {
return {
...session,
mfaVerified: true,
sws_last_seen: new Date().getTime(),
};
} else {
return session;
}
const secret = user.decryptSecret();

//check secret version
if (secret.version === 1) {
// v1 2fa verification
const verified = twofactor.verifyToken(secret.secret, token);

if (verified?.delta === 0) {
// upgrade token to v2
const newSecret = await user.generateSecret();

res.locals.type = "warn";
res.locals.message = "user authentication rejected because account mfa needs an upgrade";
res.status(403).json({
secret: newSecret.secret,
qrCode: newSecret.uri,
version: newSecret.version,
});

return;
}
} else {
// v2 2fa verification

const totp = new OTPAuth.TOTP({
issuer: "sws2apps",
label: user.user_uid,
algorithm: "SHA1",
digits: 6,
period: 30,
secret: OTPAuth.Secret.fromBase32(secret.secret),
});

await user.enableMFA();
await user.updateSessions(newSessions);

// init response object
const obj = {};
obj.message = "TOKEN_VALID";
obj.id = id;
obj.username = username;
obj.cong_name = cong_name;
obj.cong_number = cong_number;
obj.cong_role = cong_role;
obj.cong_id = cong_id;
// Validate a token.
const delta = totp.validate({
token: token,
window: 1,
});

res.locals.type = "info";
res.locals.message = "OTP token verification success";
if (delta === -1 || delta === 0 || delta === 1) {
const { visitorid } = req.headers;

let newSessions = sessions.map((session) => {
if (session.visitorid === visitorid) {
return {
...session,
mfaVerified: true,
sws_last_seen: new Date().getTime(),
};
} else {
return session;
}
});

await user.enableMFA();
await user.updateSessions(newSessions);

// init response object
const obj = {};
obj.message = "TOKEN_VALID";
obj.id = id;
obj.username = username;
obj.cong_name = cong_name;
obj.cong_number = cong_number;
obj.cong_role = cong_role;
obj.cong_id = cong_id;

res.locals.type = "info";
res.locals.message = "OTP token verification success";

res.status(200).json(obj);

return;
}
}

res.status(200).json(obj);
} else {
res.locals.type = "warn";
res.locals.message = "OTP token invalid";
res.locals.type = "warn";
res.locals.message = "OTP token invalid";

res.status(403).json({ message: "TOKEN_INVALID" });
}
res.status(403).json({ message: "TOKEN_INVALID" });
} catch (err) {
next(err);
}
Expand Down

0 comments on commit e9ce7d3

Please sign in to comment.