Updating a bunch of stuff.
This version has partially support for TwoFactorAuthentication.
This commit is contained in:
parent
d98588d1c3
commit
11f460406b
15
.vscode/launch.json
vendored
Normal file
15
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Launch Program",
|
||||
"program": "${workspaceFolder}/lib/index.js",
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/**/*.js"
|
||||
]
|
||||
}]
|
||||
}
|
@ -33,5 +33,7 @@
|
||||
"Client has no permission for acces password auth": "Dieser Client hat keine Berechtigung password auth zu benutzen",
|
||||
"Invalid token": "Ungültiger Token",
|
||||
"By clicking on ALLOW, you allow this app to access the requested recources.": "Wenn sie ALLOW drücken, berechtigen sie die Applikation die beantragten Resourcen zu benutzen.",
|
||||
"User": "User"
|
||||
"User": "User",
|
||||
"No special token": "No special token",
|
||||
"Login token invalid": "Login token invalid"
|
||||
}
|
@ -3,5 +3,11 @@
|
||||
"Username or Email": "Username or Email",
|
||||
"Password": "Password",
|
||||
"Next": "Next",
|
||||
"Invalid code": "Invalid code"
|
||||
"Invalid code": "Invalid code",
|
||||
"You are not logged in or your login is expired": "You are not logged in or your login is expired",
|
||||
"User not found": "User not found",
|
||||
"No special token": "No special token",
|
||||
"You are not logged in or your login is expired(No special token)": "You are not logged in or your login is expired(No special token)",
|
||||
"Special token invalid": "Special token invalid",
|
||||
"You are not logged in or your login is expired(Special token invalid)": "You are not logged in or your login is expired(Special token invalid)"
|
||||
}
|
1009
package-lock.json
generated
1009
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
44
package.json
44
package.json
@ -17,44 +17,44 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/body-parser": "^1.17.0",
|
||||
"@types/compression": "^0.0.36",
|
||||
"@types/compression": "^1.0.0",
|
||||
"@types/cookie-parser": "^1.4.1",
|
||||
"@types/dotenv": "^6.1.0",
|
||||
"@types/express": "^4.16.1",
|
||||
"@types/dotenv": "^6.1.1",
|
||||
"@types/express": "^4.17.0",
|
||||
"@types/i18n": "^0.8.5",
|
||||
"@types/ini": "^1.3.30",
|
||||
"@types/jsonwebtoken": "^8.3.2",
|
||||
"@types/mongodb": "^3.1.22",
|
||||
"@types/node": "^11.11.3",
|
||||
"@types/mongodb": "^3.1.31",
|
||||
"@types/node": "^12.6.9",
|
||||
"@types/node-rsa": "^1.0.0",
|
||||
"@types/qrcode": "^1.3.1",
|
||||
"@types/speakeasy": "^2.0.4",
|
||||
"@types/uuid": "^3.4.4",
|
||||
"@types/qrcode": "^1.3.3",
|
||||
"@types/speakeasy": "^2.0.5",
|
||||
"@types/uuid": "^3.4.5",
|
||||
"apidoc": "^0.17.7",
|
||||
"concurrently": "^4.1.0",
|
||||
"nodemon": "^1.18.10",
|
||||
"typescript": "^3.3.3333"
|
||||
"concurrently": "^4.1.1",
|
||||
"nodemon": "^1.19.1",
|
||||
"speakeasy": "^2.0.0",
|
||||
"typescript": "^3.5.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hibas123/nodelogging": "^1.3.21",
|
||||
"@hibas123/nodelogging": "^2.1.0",
|
||||
"@hibas123/nodeloggingserver_client": "^1.1.2",
|
||||
"@hibas123/safe_mongo": "^1.5.3",
|
||||
"body-parser": "^1.18.3",
|
||||
"compression": "^1.7.3",
|
||||
"@hibas123/safe_mongo": "^1.6.1",
|
||||
"body-parser": "^1.19.0",
|
||||
"compression": "^1.7.4",
|
||||
"cookie-parser": "^1.4.4",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^7.0.0",
|
||||
"express": "^4.16.4",
|
||||
"handlebars": "^4.1.0",
|
||||
"dotenv": "^8.0.0",
|
||||
"express": "^4.17.1",
|
||||
"handlebars": "^4.1.2",
|
||||
"i18n": "^0.8.3",
|
||||
"ini": "^1.3.5",
|
||||
"jsonwebtoken": "^8.5.0",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"moment": "^2.24.0",
|
||||
"mongodb": "^3.1.13",
|
||||
"mongodb": "^3.2.7",
|
||||
"node-rsa": "^1.0.5",
|
||||
"qrcode": "^1.3.3",
|
||||
"qrcode": "^1.4.1",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"speakeasy": "^2.0.0",
|
||||
"u2f": "^0.1.3",
|
||||
"uuid": "^3.3.2"
|
||||
}
|
||||
|
@ -3,11 +3,17 @@ import Stacker from "../middlewares/stacker";
|
||||
import { GetClientAuthMiddleware } from "../middlewares/client";
|
||||
import { GetUserMiddleware } from "../middlewares/user";
|
||||
import { createJWT } from "../../keys";
|
||||
import Client from "../../models/client";
|
||||
import RequestError, { HttpStatusCode } from "../../helper/request_error";
|
||||
|
||||
|
||||
const ClientRouter = Router();
|
||||
|
||||
/**
|
||||
* @api {get} /client/user
|
||||
*
|
||||
* @apiDescription Can be used for simple authentication of user. It will redirect the user to the redirect URI with a very short lived jwt.
|
||||
*
|
||||
* @apiParam {String} redirect_uri URL to redirect to on success
|
||||
* @apiParam {String} state A optional state, that will be included in the JWT and redirect_uri as parameter
|
||||
*
|
||||
@ -18,6 +24,10 @@ const ClientRouter = Router();
|
||||
*/
|
||||
ClientRouter.get("/user", Stacker(GetClientAuthMiddleware(false), GetUserMiddleware(false, false), async (req: Request, res: Response) => {
|
||||
let { redirect_uri, state } = req.query;
|
||||
|
||||
if (redirect_uri !== req.client.redirect_url)
|
||||
throw new RequestError("Invalid redirect URI", HttpStatusCode.BAD_REQUEST);
|
||||
|
||||
let jwt = await createJWT({
|
||||
client: req.client.client_id,
|
||||
uid: req.user.uid,
|
||||
|
@ -11,26 +11,33 @@ class Invalid extends Error { }
|
||||
* Returns customized Middleware function, that could also be called directly
|
||||
* by code and will return true or false depending on the token. In the false
|
||||
* case it will also send error and redirect if json is not set
|
||||
* @param json Checks if requests wants an json or html for returning errors
|
||||
* @param redirect_uri Sets the uri to redirect, if json is not set and user not logged in
|
||||
* @param json Default false. Checks if requests wants an json or html for returning errors
|
||||
* @param special_required Default false. If true, a special token is required
|
||||
* @param redirect_uri Default current uri. Sets the uri to redirect, if json is not set and user not logged in
|
||||
* @param validated Default true. If false, the token must not be validated
|
||||
*/
|
||||
export function GetUserMiddleware(json = false, special_required: boolean = false, redirect_uri?: string, validated = true) {
|
||||
return promiseMiddleware(async function (req: Request, res: Response, next?: NextFunction) {
|
||||
const invalid = () => {
|
||||
throw new Invalid();
|
||||
const invalid = (message: string) => {
|
||||
throw new Invalid(req.__(message));
|
||||
}
|
||||
try {
|
||||
let { login, special } = req.cookies
|
||||
if (!login) invalid()
|
||||
if (!login) {
|
||||
login = req.query.login;
|
||||
special = req.query.special;
|
||||
}
|
||||
if (!login) invalid("No login token")
|
||||
if (!special && special_required) invalid("No special token")
|
||||
|
||||
let token = await LoginToken.findOne({ token: login, valid: true })
|
||||
if (!await CheckToken(token, validated)) invalid();
|
||||
if (!await CheckToken(token, validated)) invalid("Login token invalid");
|
||||
|
||||
let user = await User.findById(token.user);
|
||||
if (!user) {
|
||||
token.valid = false;
|
||||
await LoginToken.save(token);
|
||||
invalid();
|
||||
invalid("Login token invalid");
|
||||
}
|
||||
|
||||
let special_token;
|
||||
@ -38,12 +45,10 @@ export function GetUserMiddleware(json = false, special_required: boolean = fals
|
||||
Logging.debug("Special found")
|
||||
special_token = await LoginToken.findOne({ token: special, special: true, valid: true, user: token.user })
|
||||
if (!await CheckToken(special_token, validated))
|
||||
invalid();
|
||||
invalid("Special token invalid");
|
||||
req.special = true;
|
||||
}
|
||||
|
||||
if (special_required && !req.special) invalid();
|
||||
|
||||
req.user = user
|
||||
req.isAdmin = user.admin;
|
||||
req.token = {
|
||||
@ -60,7 +65,7 @@ export function GetUserMiddleware(json = false, special_required: boolean = fals
|
||||
res.status(HttpStatusCode.UNAUTHORIZED)
|
||||
res.redirect("/login?base64=true&state=" + Buffer.from(redirect_uri ? redirect_uri : req.originalUrl).toString("base64"))
|
||||
} else {
|
||||
throw new RequestError(req.__("You are not logged in or your login is expired"), HttpStatusCode.UNAUTHORIZED)
|
||||
throw new RequestError(req.__("You are not logged in or your login is expired" + ` (${e.message})`), HttpStatusCode.UNAUTHORIZED, undefined, { auth: true })
|
||||
}
|
||||
} else {
|
||||
if (next) next(e);
|
||||
|
16
src/api/user/account.ts
Normal file
16
src/api/user/account.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { Request, Response } from "express";
|
||||
import Stacker from "../middlewares/stacker";
|
||||
import { GetUserMiddleware } from "../middlewares/user";
|
||||
import LoginToken, { CheckToken } from "../../models/login_token";
|
||||
import RequestError, { HttpStatusCode } from "../../helper/request_error";
|
||||
|
||||
export const GetAccount = Stacker(GetUserMiddleware(true, true), async (req: Request, res: Response) => {
|
||||
let user = {
|
||||
id: req.user.uid,
|
||||
name: req.user.name,
|
||||
username: req.user.username,
|
||||
birthday: req.user.birthday,
|
||||
gender: req.user.gender,
|
||||
};
|
||||
res.json({ user });
|
||||
});
|
@ -3,6 +3,7 @@ import Register from "./register";
|
||||
import Login from "./login";
|
||||
import TwoFactorRoute from "./twofactor";
|
||||
import { GetToken, DeleteToken } from "./token";
|
||||
import { GetAccount } from "./account";
|
||||
|
||||
const UserRoute: Router = Router();
|
||||
|
||||
@ -84,7 +85,6 @@ UserRoute.get("/token", GetToken);
|
||||
*
|
||||
* @apiName UserDeleteToken
|
||||
*
|
||||
* @apiParam {String} type Type could be either "username" or "password"
|
||||
*
|
||||
* @apiGroup user
|
||||
* @apiPermission user
|
||||
@ -92,4 +92,22 @@ UserRoute.get("/token", GetToken);
|
||||
* @apiSuccess {Boolean} success
|
||||
*/
|
||||
UserRoute.delete("/token/:id", DeleteToken);
|
||||
|
||||
|
||||
/**
|
||||
* @api {delete} /user/account
|
||||
* @apiName UserGetAccount
|
||||
*
|
||||
* @apiGroup user
|
||||
* @apiPermission user
|
||||
*
|
||||
* @apiSuccess {Boolean} success
|
||||
* @apiSuccess {Object[]} user
|
||||
* @apiSuccess {String} user.id User ID
|
||||
* @apiSuccess {String} token.name Full name of the user
|
||||
* @apiSuccess {String} token.username Username of user
|
||||
* @apiSuccess {Date} token.birthday Birthday
|
||||
* @apiSuccess {Number} token.gender Gender of user (none = 0, male = 1, female = 2, other = 3)
|
||||
*/
|
||||
UserRoute.get("/account", GetAccount);
|
||||
export default UserRoute;
|
@ -4,7 +4,7 @@ import { randomBytes } from "crypto";
|
||||
import moment = require("moment");
|
||||
import LoginToken from "../../models/login_token";
|
||||
import promiseMiddleware from "../../helper/promiseMiddleware";
|
||||
import TwoFactor from "../../models/twofactor";
|
||||
import TwoFactor, { TFATypes, TFANames } from "../../models/twofactor";
|
||||
|
||||
const Login = promiseMiddleware(async (req: Request, res: Response) => {
|
||||
let type = req.query.type;
|
||||
@ -58,8 +58,6 @@ const Login = promiseMiddleware(async (req: Request, res: Response) => {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
let { username, password, uid } = req.body;
|
||||
|
||||
let user = await User.findOne(username ? { username: username.toLowerCase() } : { uid: uid })
|
||||
@ -80,7 +78,7 @@ const Login = promiseMiddleware(async (req: Request, res: Response) => {
|
||||
let tfa = twofactor.map(e => {
|
||||
return {
|
||||
id: e._id,
|
||||
name: e.name,
|
||||
name: e.name || TFANames.get(e.type),
|
||||
type: e.type
|
||||
}
|
||||
})
|
||||
|
@ -20,7 +20,7 @@ export const GetToken = Stacker(GetUserMiddleware(true, true), async (req: Reque
|
||||
});
|
||||
|
||||
export const DeleteToken = Stacker(GetUserMiddleware(true, true), async (req: Request, res: Response) => {
|
||||
let { id } = req.query;
|
||||
let { id } = req.params;
|
||||
let token = await LoginToken.findById(id);
|
||||
if (!token || !token.user.equals(req.user._id)) throw new RequestError("Invalid ID", HttpStatusCode.BAD_REQUEST);
|
||||
token.valid = false;
|
||||
|
74
src/api/user/twofactor/backup/index.ts
Normal file
74
src/api/user/twofactor/backup/index.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import { Router } from "express"
|
||||
import Stacker from "../../../middlewares/stacker";
|
||||
import { GetUserMiddleware } from "../../../middlewares/user";
|
||||
import TwoFactor, { TFATypes as TwoFATypes, IBackupCode } from "../../../../models/twofactor";
|
||||
import RequestError, { HttpStatusCode } from "../../../../helper/request_error";
|
||||
import moment = require("moment");
|
||||
import { upgradeToken } from "../helper";
|
||||
import * as crypto from "crypto";
|
||||
import Logging from "@hibas123/nodelogging";
|
||||
|
||||
const BackupCodeRoute = Router();
|
||||
|
||||
// TODO: Further checks if this is good enough randomness
|
||||
function generateCode(length: number) {
|
||||
let bytes = crypto.randomBytes(length);
|
||||
let nrs = "";
|
||||
bytes.forEach((b, idx) => {
|
||||
let nr = Math.floor((b / 255) * 9.9999)
|
||||
if (nr > 9) nr = 9;
|
||||
nrs += String(nr);
|
||||
})
|
||||
return nrs;
|
||||
}
|
||||
|
||||
BackupCodeRoute.post("/", Stacker(GetUserMiddleware(true, true), async (req, res) => {
|
||||
//Generating new
|
||||
let codes = Array(10).map(() => generateCode(8));
|
||||
console.log(codes);
|
||||
let twofactor = TwoFactor.new(<IBackupCode>{
|
||||
user: req.user._id,
|
||||
type: TwoFATypes.OTC,
|
||||
valid: true,
|
||||
data: codes,
|
||||
name: ""
|
||||
})
|
||||
await TwoFactor.save(twofactor);
|
||||
res.json({
|
||||
codes,
|
||||
id: twofactor._id
|
||||
})
|
||||
}));
|
||||
|
||||
BackupCodeRoute.put("/", Stacker(GetUserMiddleware(true, false, undefined, false), async (req, res) => {
|
||||
let { login, special } = req.token;
|
||||
let { id, code }: { id: string, code: string } = req.body;
|
||||
|
||||
let twofactor: IBackupCode = await TwoFactor.findById(id);
|
||||
|
||||
if (!twofactor || !twofactor.valid || !twofactor.user.equals(req.user._id) || twofactor.type !== TwoFATypes.OTC) {
|
||||
throw new RequestError("Invalid Method!", HttpStatusCode.BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (twofactor.expires && moment().isAfter(twofactor.expires)) {
|
||||
twofactor.valid = false;
|
||||
await TwoFactor.save(twofactor);
|
||||
throw new RequestError("Invalid Method!", HttpStatusCode.BAD_REQUEST);
|
||||
}
|
||||
code = code.replace(/\s/g, "");
|
||||
let valid = twofactor.data.find(c => c === code);
|
||||
|
||||
if (valid) {
|
||||
twofactor.data = twofactor.data.filter(c => c !== code);
|
||||
await TwoFactor.save(twofactor);
|
||||
let [login_exp, special_exp] = await Promise.all([
|
||||
upgradeToken(login),
|
||||
upgradeToken(special)
|
||||
]);
|
||||
res.json({ success: true, login_exp, special_exp })
|
||||
} else {
|
||||
throw new RequestError("Invalid or already used code!", HttpStatusCode.BAD_REQUEST);
|
||||
}
|
||||
}))
|
||||
|
||||
export default BackupCodeRoute;
|
@ -1,72 +1,6 @@
|
||||
import LoginToken, { ILoginToken } from "../../../models/login_token";
|
||||
import moment = require("moment");
|
||||
|
||||
// export async function unlockToken() {
|
||||
// let { type, code, login, special } = req.body;
|
||||
|
||||
// let [login_t, special_t] = await Promise.all([LoginToken.findOne({ token: login }), LoginToken.findOne({ token: special })]);
|
||||
|
||||
// if ((login && !login_t) || (special && !special_t)) {
|
||||
// res.json({ error: req.__("Token not found!") });
|
||||
// } else {
|
||||
// let atoken = special_t || login_t;
|
||||
|
||||
// let user = await User.findById(atoken.user);
|
||||
|
||||
// let tf = await TwoFactor.find({ user: user._id, valid: true })
|
||||
|
||||
// let valid = false;
|
||||
// switch (type) {
|
||||
// case TokenTypes.OTC: {
|
||||
// let twofactor = await TwoFactor.findOne({ type, valid: true })
|
||||
// if (twofactor) {
|
||||
// valid = speakeasy.totp.verify({
|
||||
// secret: twofactor.token,
|
||||
// encoding: "base64",
|
||||
// token: code
|
||||
// })
|
||||
// }
|
||||
// break;
|
||||
// }
|
||||
|
||||
// case TokenTypes.BACKUP_CODE: {
|
||||
// let twofactor = await TwoFactor.findOne({ type, valid: true, token: code })
|
||||
// if (twofactor) {
|
||||
// twofactor.valid = false;
|
||||
// await TwoFactor.save(twofactor);
|
||||
// valid = true;
|
||||
// }
|
||||
// break;
|
||||
// }
|
||||
// case TokenTypes.APP_ALLOW:
|
||||
// case TokenTypes.YUBI_KEY:
|
||||
// default:
|
||||
// res.json({ error: req.__("Invalid twofactor!") });
|
||||
// return;
|
||||
|
||||
// }
|
||||
|
||||
// if (!valid) {
|
||||
// res.json({ error: req.__("Invalid code!") });
|
||||
// return;
|
||||
// }
|
||||
|
||||
// let result: any = {};
|
||||
// if (login_t) {
|
||||
// login_t.validated = true
|
||||
// await LoginToken.save(login_t)
|
||||
// result.login = { token: login_t.token, expires: login_t.validTill.toUTCString() }
|
||||
// }
|
||||
|
||||
// if (special_t) {
|
||||
// special_t.validated = true;
|
||||
// await LoginToken.save(special_t);
|
||||
// result.special = { token: special_t.token, expires: special_t.validTill.toUTCString() }
|
||||
// }
|
||||
// res.json(result);
|
||||
// }
|
||||
// }
|
||||
|
||||
export async function upgradeToken(token: ILoginToken) {
|
||||
token.data = undefined;
|
||||
token.valid = true;
|
||||
|
@ -5,6 +5,8 @@ import Stacker from "../../middlewares/stacker";
|
||||
import TwoFactor from "../../../models/twofactor";
|
||||
import * as moment from "moment"
|
||||
import RequestError, { HttpStatusCode } from "../../../helper/request_error";
|
||||
import OTCRoute from "./otc";
|
||||
import BackupCodeRoute from "./backup";
|
||||
|
||||
const TwoFactorRouter = Router();
|
||||
|
||||
@ -26,8 +28,8 @@ TwoFactorRouter.get("/", Stacker(GetUserMiddleware(true, true), async (req, res)
|
||||
res.json({ methods: tfa });
|
||||
}));
|
||||
|
||||
TwoFactorRouter.delete("/", Stacker(GetUserMiddleware(true, true), async (req, res) => {
|
||||
let { id } = req.query;
|
||||
TwoFactorRouter.delete("/:id", Stacker(GetUserMiddleware(true, true), async (req, res) => {
|
||||
let { id } = req.params;
|
||||
let tfa = await TwoFactor.findById(id);
|
||||
if (!tfa || !tfa.user.equals(req.user._id)) {
|
||||
throw new RequestError("Invalid id", HttpStatusCode.BAD_REQUEST);
|
||||
@ -38,5 +40,7 @@ TwoFactorRouter.delete("/", Stacker(GetUserMiddleware(true, true), async (req, r
|
||||
}));
|
||||
|
||||
TwoFactorRouter.use("/yubikey", YubiKeyRoute);
|
||||
TwoFactorRouter.use("/otc", OTCRoute);
|
||||
TwoFactorRouter.use("/backup", BackupCodeRoute);
|
||||
|
||||
export default TwoFactorRouter;
|
105
src/api/user/twofactor/otc/index.ts
Normal file
105
src/api/user/twofactor/otc/index.ts
Normal file
@ -0,0 +1,105 @@
|
||||
import { Router } from "express"
|
||||
import Stacker from "../../../middlewares/stacker";
|
||||
import { GetUserMiddleware } from "../../../middlewares/user";
|
||||
import TwoFactor, { TFATypes as TwoFATypes, IOTC } from "../../../../models/twofactor";
|
||||
import RequestError, { HttpStatusCode } from "../../../../helper/request_error";
|
||||
import moment = require("moment");
|
||||
import { upgradeToken } from "../helper";
|
||||
import Logging from "@hibas123/nodelogging";
|
||||
|
||||
import * as speakeasy from "speakeasy";
|
||||
import * as qrcode from "qrcode";
|
||||
import config from "../../../../config";
|
||||
|
||||
const OTCRoute = Router();
|
||||
|
||||
OTCRoute.post("/", Stacker(GetUserMiddleware(true, true), async (req, res) => {
|
||||
const { type } = req.query;
|
||||
if (type === "create") {
|
||||
//Generating new
|
||||
let secret = speakeasy.generateSecret({
|
||||
name: config.core.name,
|
||||
issuer: config.core.name
|
||||
});
|
||||
let twofactor = TwoFactor.new(<IOTC>{
|
||||
user: req.user._id,
|
||||
type: TwoFATypes.OTC,
|
||||
valid: false,
|
||||
data: secret.base32
|
||||
})
|
||||
let dataurl = await qrcode.toDataURL(secret.otpauth_url);
|
||||
await TwoFactor.save(twofactor);
|
||||
res.json({
|
||||
image: dataurl,
|
||||
id: twofactor._id
|
||||
})
|
||||
} else if (type === "validate") {
|
||||
// Checking code and marking as valid
|
||||
const { code, id } = req.body;
|
||||
Logging.debug(req.body, id);
|
||||
let twofactor: IOTC = await TwoFactor.findById(id);
|
||||
const err = () => { throw new RequestError("Invalid ID!", HttpStatusCode.BAD_REQUEST) };
|
||||
if (!twofactor || !twofactor.user.equals(req.user._id) || twofactor.type !== TwoFATypes.OTC || !twofactor.data || twofactor.valid) {
|
||||
Logging.debug("Not found or wrong user", twofactor);
|
||||
err();
|
||||
}
|
||||
|
||||
if (twofactor.expires && moment().isAfter(moment(twofactor.expires))) {
|
||||
await TwoFactor.delete(twofactor);
|
||||
Logging.debug("Expired!", twofactor);
|
||||
err();
|
||||
}
|
||||
|
||||
let valid = speakeasy.totp.verify({
|
||||
secret: twofactor.data,
|
||||
encoding: "base32",
|
||||
token: code
|
||||
})
|
||||
|
||||
if (valid) {
|
||||
twofactor.expires = undefined;
|
||||
twofactor.valid = true;
|
||||
await TwoFactor.save(twofactor);
|
||||
res.json({ success: true });
|
||||
} else {
|
||||
throw new RequestError("Invalid Code!", HttpStatusCode.BAD_REQUEST);
|
||||
}
|
||||
} else {
|
||||
throw new RequestError("Invalid type", HttpStatusCode.BAD_REQUEST);
|
||||
}
|
||||
}));
|
||||
|
||||
OTCRoute.put("/", Stacker(GetUserMiddleware(true, false, undefined, false), async (req, res) => {
|
||||
let { login, special } = req.token;
|
||||
let { id, code } = req.body;
|
||||
let twofactor: IOTC = await TwoFactor.findById(id);
|
||||
|
||||
if (!twofactor || !twofactor.valid || !twofactor.user.equals(req.user._id) || twofactor.type !== TwoFATypes.OTC) {
|
||||
throw new RequestError("Invalid Method!", HttpStatusCode.BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (twofactor.expires && moment().isAfter(twofactor.expires)) {
|
||||
twofactor.valid = false;
|
||||
await TwoFactor.save(twofactor);
|
||||
throw new RequestError("Invalid Method!", HttpStatusCode.BAD_REQUEST);
|
||||
}
|
||||
|
||||
|
||||
let valid = speakeasy.totp.verify({
|
||||
secret: twofactor.data,
|
||||
encoding: "base32",
|
||||
token: code
|
||||
})
|
||||
|
||||
if (valid) {
|
||||
let [login_exp, special_exp] = await Promise.all([
|
||||
upgradeToken(login),
|
||||
upgradeToken(special)
|
||||
]);
|
||||
res.json({ success: true, login_exp, special_exp })
|
||||
} else {
|
||||
throw new RequestError("Invalid Code", HttpStatusCode.BAD_REQUEST);
|
||||
}
|
||||
}))
|
||||
|
||||
export default OTCRoute;
|
@ -12,6 +12,9 @@ import Logging from "@hibas123/nodelogging";
|
||||
|
||||
const U2FRoute = Router();
|
||||
|
||||
/**
|
||||
* Registerinf a new YubiKey
|
||||
*/
|
||||
U2FRoute.post("/", Stacker(GetUserMiddleware(true, true), async (req, res) => {
|
||||
const { type } = req.query;
|
||||
if (type === "challenge") {
|
||||
|
@ -381,7 +381,7 @@ export enum HttpStatusCode {
|
||||
|
||||
|
||||
export default class RequestError extends Error {
|
||||
constructor(message: any, public status: HttpStatusCode, public nolog: boolean = false) {
|
||||
constructor(message: any, public status: HttpStatusCode, public nolog: boolean = false, public additional: any = undefined) {
|
||||
super("")
|
||||
this.message = message;
|
||||
}
|
||||
|
14
src/index.ts
14
src/index.ts
@ -37,6 +37,8 @@ DB.connect().then(async () => {
|
||||
await TestData()
|
||||
let web = new Web(config.web)
|
||||
web.listen()
|
||||
|
||||
let already = new Set();
|
||||
function print(path, layer) {
|
||||
if (layer.route) {
|
||||
layer.route.stack.forEach(print.bind(null, path.concat(split(layer.route.path))))
|
||||
@ -45,7 +47,11 @@ DB.connect().then(async () => {
|
||||
} else if (layer.method) {
|
||||
let me: string = layer.method.toUpperCase();
|
||||
me += " ".repeat(6 - me.length);
|
||||
Logging.log(`${me} /${path.concat(split(layer.regexp)).filter(Boolean).join('/')}`);
|
||||
let msg = `${me} /${path.concat(split(layer.regexp)).filter(Boolean).join('/')}`;
|
||||
if (!already.has(msg)) {
|
||||
already.add(msg);
|
||||
Logging.log(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -64,9 +70,9 @@ DB.connect().then(async () => {
|
||||
: '<complex:' + thing.toString() + '>'
|
||||
}
|
||||
}
|
||||
Logging.log("--- Endpoints: ---");
|
||||
web.server._router.stack.forEach(print.bind(null, []))
|
||||
Logging.log("--- Endpoints end ---")
|
||||
// Logging.log("--- Endpoints: ---");
|
||||
// web.server._router.stack.forEach(print.bind(null, []))
|
||||
// Logging.log("--- Endpoints end ---")
|
||||
}).catch(e => {
|
||||
Logging.error(e)
|
||||
process.exit();
|
||||
|
@ -52,8 +52,10 @@ const LoginToken = DB.addModel<ILoginToken>({
|
||||
})
|
||||
|
||||
export async function CheckToken(token: ILoginToken, validated: boolean = true): Promise<boolean> {
|
||||
if (!token || !token.valid) return false;
|
||||
if (validated && !token.validated) return false;
|
||||
if (!token || !token.valid)
|
||||
return false;
|
||||
if (validated && !token.validated)
|
||||
return false;
|
||||
if (moment().isAfter(token.validTill)) {
|
||||
token.valid = false;
|
||||
await LoginToken.save(token)
|
||||
|
@ -9,6 +9,12 @@ export enum TFATypes {
|
||||
APP_ALLOW
|
||||
}
|
||||
|
||||
export const TFANames = new Map<TFATypes, string>();
|
||||
TFANames.set(TFATypes.OTC, "Authenticator");
|
||||
TFANames.set(TFATypes.BACKUP_CODE, "Backup Codes");
|
||||
TFANames.set(TFATypes.U2F, "Security Key (U2F)");
|
||||
TFANames.set(TFATypes.APP_ALLOW, "App Push");
|
||||
|
||||
export interface ITwoFactor extends ModelDataBase {
|
||||
user: ObjectID
|
||||
valid: boolean
|
||||
@ -18,7 +24,7 @@ export interface ITwoFactor extends ModelDataBase {
|
||||
data: any;
|
||||
}
|
||||
|
||||
export interface IOTP extends ITwoFactor {
|
||||
export interface IOTC extends ITwoFactor {
|
||||
data: string;
|
||||
}
|
||||
|
||||
@ -39,6 +45,10 @@ export interface IU2F extends ITwoFactor {
|
||||
}
|
||||
}
|
||||
|
||||
export interface IBackupCode extends ITwoFactor {
|
||||
data: string[];
|
||||
}
|
||||
|
||||
const TwoFactor = DB.addModel<ITwoFactor>({
|
||||
name: "twofactor",
|
||||
versions: [{
|
||||
|
@ -8,6 +8,11 @@ import { ObjectID } from "bson";
|
||||
import DB from "./database";
|
||||
import TwoFactor from "./models/twofactor";
|
||||
|
||||
|
||||
import * as speakeasy from "speakeasy";
|
||||
import LoginToken from "./models/login_token";
|
||||
import { log } from "handlebars";
|
||||
|
||||
export default async function TestData() {
|
||||
await DB.db.dropDatabase();
|
||||
let u = await User.findOne({ username: "test" });
|
||||
@ -63,18 +68,56 @@ export default async function TestData() {
|
||||
await RegCode.save(r);
|
||||
}
|
||||
|
||||
let t = await TwoFactor.findOne({ user: u._id, type: 2 })
|
||||
let t = await TwoFactor.findOne({ user: u._id, type: 0 })
|
||||
if (!t) {
|
||||
t = TwoFactor.new({
|
||||
user: u._id,
|
||||
type: 2,
|
||||
type: 0,
|
||||
valid: true,
|
||||
data: {
|
||||
keyHandle: "tWSaMoHX2E96CoZOKOi_4aj6WVEh1e46FKXN0oDY2Z-laNOFcATlStNDo52HX7ygupW-v9qZOCX3J4d5nhOzWQ",
|
||||
publicKey: "BPsgBxR8M7MyrknlFuvYZv0Z1lZxiJQJNrLDA1yi3XKD_lrhIpnAh2OY_TsFjASvn3JTtwlCh62QdMvN-ejQL78"
|
||||
},
|
||||
data: "IIRW2P2UJRDDO2LDIRYW4LSREZLWMOKDNBJES2LLHRREK3R6KZJQ",
|
||||
expires: null
|
||||
})
|
||||
TwoFactor.save(t);
|
||||
}
|
||||
|
||||
let login_token = await LoginToken.findOne({ token: "test01" });
|
||||
if (login_token)
|
||||
await LoginToken.delete(login_token);
|
||||
|
||||
login_token = LoginToken.new({
|
||||
browser: "DEMO",
|
||||
ip: "10.0.0.1",
|
||||
special: false,
|
||||
token: "test01",
|
||||
valid: true,
|
||||
validTill: moment().add("10", "years").toDate(),
|
||||
user: u._id,
|
||||
validated: true
|
||||
});
|
||||
await LoginToken.save(login_token);
|
||||
|
||||
let special_token = await LoginToken.findOne({ token: "test02" });
|
||||
if (special_token)
|
||||
await LoginToken.delete(special_token);
|
||||
|
||||
special_token = LoginToken.new({
|
||||
browser: "DEMO",
|
||||
ip: "10.0.0.1",
|
||||
special: true,
|
||||
token: "test02",
|
||||
valid: true,
|
||||
validTill: moment().add("10", "years").toDate(),
|
||||
user: u._id,
|
||||
validated: true
|
||||
});
|
||||
await LoginToken.save(special_token);
|
||||
|
||||
|
||||
// setInterval(() => {
|
||||
// let code = speakeasy.totp({
|
||||
// secret: t.data,
|
||||
// encoding: "base32"
|
||||
// })
|
||||
// Logging.debug("OTC Code is:", code);
|
||||
// }, 1000)
|
||||
}
|
@ -88,12 +88,12 @@ export default class Web {
|
||||
if (error.status === 500 && !(<any>error).nolog) {
|
||||
Logging.error(error);
|
||||
} else {
|
||||
Logging.log(typeof error.message === "string" ? error.message.split("\n", 1)[0] : error.message);
|
||||
Logging.log("Responded with Error:", typeof error.message === "string" ? error.message.split("\n", 1)[0] : error.message);
|
||||
}
|
||||
|
||||
if (req.accepts(["json"])) {
|
||||
res.json_status = error.status || 500;
|
||||
res.json({ error: error.message, status: error.status || 500 })
|
||||
res.json({ error: error.message, status: error.status || 500, additional: error.additional })
|
||||
} else
|
||||
res.status(error.status || 500).send(error.message)
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user