Added U2F Support for YubiKey
This commit is contained in:
@ -1,8 +1,8 @@
|
||||
import { Request, Router } from "express";
|
||||
import ClientRoute from "./admin/client";
|
||||
import UserRoute from "./admin/user";
|
||||
import RegCodeRoute from "./admin/regcode";
|
||||
import PermissionRoute from "./admin/permission";
|
||||
import ClientRoute from "./client";
|
||||
import UserRoute from "./user";
|
||||
import RegCodeRoute from "./regcode";
|
||||
import PermissionRoute from "./permission";
|
||||
|
||||
const AdminRoute: Router = Router();
|
||||
AdminRoute.use("/client", ClientRoute);
|
@ -14,6 +14,9 @@ ApiRouter.use("/user", UserRoute);
|
||||
ApiRouter.use("/internal", InternalRoute);
|
||||
ApiRouter.use("/oauth", OAuthRoute);
|
||||
|
||||
ApiRouter.use("/client/user", AuthGetUser);
|
||||
|
||||
// Legacy reasons (deprecated)
|
||||
ApiRouter.use("/user", AuthGetUser);
|
||||
|
||||
// Legacy reasons (deprecated)
|
@ -1,6 +1,6 @@
|
||||
import { Router } from "express";
|
||||
import { OAuthInternalApp } from "./internal/oauth";
|
||||
import PasswordAuth from "./internal/password";
|
||||
import { OAuthInternalApp } from "./oauth";
|
||||
import PasswordAuth from "./password";
|
||||
|
||||
const InternalRoute: Router = Router();
|
||||
InternalRoute.get("/oauth", OAuthInternalApp);
|
@ -1,7 +1,9 @@
|
||||
import { Request, Response, NextFunction, RequestHandler } from "express";
|
||||
import promiseMiddleware from "../../helper/promiseMiddleware";
|
||||
|
||||
function call(handler: RequestHandler, req: Request, res: Response) {
|
||||
type RH = (req: Request, res: Response, next?: NextFunction) => any;
|
||||
|
||||
function call(handler: RH, req: Request, res: Response) {
|
||||
return new Promise((yes, no) => {
|
||||
let p = handler(req, res, (err) => {
|
||||
if (err) no(err);
|
||||
@ -11,7 +13,7 @@ function call(handler: RequestHandler, req: Request, res: Response) {
|
||||
})
|
||||
}
|
||||
|
||||
const Stacker = (...handler: RequestHandler[]) => {
|
||||
const Stacker = (...handler: RH[]) => {
|
||||
return promiseMiddleware(async (req: Request, res: Response, next: NextFunction) => {
|
||||
let hc = handler.concat();
|
||||
while (hc.length > 0) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import LoginToken from "../../models/login_token";
|
||||
import LoginToken, { CheckToken } from "../../models/login_token";
|
||||
import Logging from "@hibas123/nodelogging";
|
||||
import RequestError, { HttpStatusCode } from "../../helper/request_error";
|
||||
import User from "../../models/user";
|
||||
@ -14,7 +14,7 @@ class Invalid extends Error { }
|
||||
* @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
|
||||
*/
|
||||
export function GetUserMiddleware(json = false, special_token: boolean = false, redirect_uri?: string) {
|
||||
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();
|
||||
@ -24,8 +24,7 @@ export function GetUserMiddleware(json = false, special_token: boolean = false,
|
||||
if (!login) invalid()
|
||||
|
||||
let token = await LoginToken.findOne({ token: login, valid: true })
|
||||
if (!token) invalid()
|
||||
if (!token.validated) invalid();
|
||||
if (!await CheckToken(token, validated)) invalid();
|
||||
|
||||
let user = await User.findById(token.user);
|
||||
if (!user) {
|
||||
@ -34,31 +33,23 @@ export function GetUserMiddleware(json = false, special_token: boolean = false,
|
||||
invalid();
|
||||
}
|
||||
|
||||
if (token.validTill.getTime() < new Date().getTime()) { //Token expired
|
||||
token.valid = false;
|
||||
await LoginToken.save(token);
|
||||
invalid()
|
||||
}
|
||||
|
||||
let special_token;
|
||||
if (special) {
|
||||
Logging.debug("Special found")
|
||||
let st = await LoginToken.findOne({ token: special, special: true, valid: true })
|
||||
if (st && st.validated && st.valid && st.user.toHexString() === token.user.toHexString()) {
|
||||
if (st.validTill.getTime() < new Date().getTime()) { //Token expired
|
||||
Logging.debug("Special expired")
|
||||
st.valid = false;
|
||||
await LoginToken.save(st);
|
||||
} else {
|
||||
Logging.debug("Special valid")
|
||||
req.special = true;
|
||||
}
|
||||
}
|
||||
special_token = await LoginToken.findOne({ token: special, special: true, valid: true, user: token.user })
|
||||
if (!await CheckToken(special_token, validated))
|
||||
invalid();
|
||||
req.special = true;
|
||||
}
|
||||
|
||||
if (special_token && !req.special) invalid();
|
||||
if (special_required && !req.special) invalid();
|
||||
|
||||
req.user = user
|
||||
req.isAdmin = user.admin;
|
||||
req.token = {
|
||||
login: token,
|
||||
special: special_token
|
||||
}
|
||||
|
||||
if (next)
|
||||
next()
|
||||
@ -67,7 +58,7 @@ export function GetUserMiddleware(json = false, special_token: boolean = false,
|
||||
if (e instanceof Invalid) {
|
||||
if (req.method === "GET" && !json) {
|
||||
res.status(HttpStatusCode.UNAUTHORIZED)
|
||||
res.redirect("/login?base64=true&state=" + new Buffer(redirect_uri ? redirect_uri : req.originalUrl).toString("base64"))
|
||||
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)
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { Router } from "express";
|
||||
import AuthRoute from "./oauth/auth";
|
||||
import JWTRoute from "./oauth/jwt";
|
||||
import Public from "./oauth/public";
|
||||
import RefreshTokenRoute from "./oauth/refresh";
|
||||
import AuthRoute from "./auth";
|
||||
import JWTRoute from "./jwt";
|
||||
import Public from "./public";
|
||||
import RefreshTokenRoute from "./refresh";
|
||||
|
||||
const OAuthRoue: Router = Router();
|
||||
OAuthRoue.post("/auth", AuthRoute);
|
@ -1,8 +0,0 @@
|
||||
import { Request, Router } from "express";
|
||||
import Register from "./user/register";
|
||||
import Login from "./user/login";
|
||||
|
||||
const UserRoute: Router = Router();
|
||||
UserRoute.post("/register", Register);
|
||||
UserRoute.post("/login", Login)
|
||||
export default UserRoute;
|
14
src/api/user/index.ts
Normal file
14
src/api/user/index.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { Router } from "express";
|
||||
import Register from "./register";
|
||||
import Login from "./login";
|
||||
import TwoFactorRoute from "./twofactor";
|
||||
import { GetToken, DeleteToken } from "./token";
|
||||
|
||||
const UserRoute: Router = Router();
|
||||
UserRoute.post("/register", Register);
|
||||
UserRoute.post("/login", Login)
|
||||
UserRoute.use("/twofactor", TwoFactorRoute);
|
||||
|
||||
UserRoute.get("/token", GetToken);
|
||||
UserRoute.delete("/token", DeleteToken);
|
||||
export default UserRoute;
|
@ -1,10 +1,12 @@
|
||||
import { Request, Response } from "express"
|
||||
import User, { IUser, TokenTypes } from "../../models/user";
|
||||
import User, { IUser } from "../../models/user";
|
||||
import { randomBytes } from "crypto";
|
||||
import moment = require("moment");
|
||||
import LoginToken from "../../models/login_token";
|
||||
import RequestError, { HttpStatusCode } from "../../helper/request_error";
|
||||
import promiseMiddleware from "../../helper/promiseMiddleware";
|
||||
import * as speakeasy from "speakeasy";
|
||||
import TwoFactor from "../../models/twofactor";
|
||||
|
||||
const Login = promiseMiddleware(async (req: Request, res: Response) => {
|
||||
let type = req.query.type;
|
||||
@ -19,7 +21,13 @@ const Login = promiseMiddleware(async (req: Request, res: Response) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const sendToken = async (user: IUser, tfa?: TokenTypes[]) => {
|
||||
const sendToken = async (user: IUser, tfa?: any[]) => {
|
||||
let ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress
|
||||
let client = {
|
||||
ip: Array.isArray(ip) ? ip[0] : ip,
|
||||
browser: req.headers["user-agent"]
|
||||
}
|
||||
|
||||
let token_str = randomBytes(16).toString("hex");
|
||||
let tfa_exp = moment().add(5, "minutes").toDate()
|
||||
let token_exp = moment().add(6, "months").toDate()
|
||||
@ -28,7 +36,8 @@ const Login = promiseMiddleware(async (req: Request, res: Response) => {
|
||||
valid: true,
|
||||
validTill: tfa ? tfa_exp : token_exp,
|
||||
user: user._id,
|
||||
validated: tfa ? false : true
|
||||
validated: tfa ? false : true,
|
||||
...client
|
||||
});
|
||||
await LoginToken.save(token);
|
||||
|
||||
@ -40,7 +49,8 @@ const Login = promiseMiddleware(async (req: Request, res: Response) => {
|
||||
validTill: tfa ? tfa_exp : special_exp,
|
||||
special: true,
|
||||
user: user._id,
|
||||
validated: tfa ? false : true
|
||||
validated: tfa ? false : true,
|
||||
...client
|
||||
});
|
||||
await LoginToken.save(special);
|
||||
|
||||
@ -51,7 +61,7 @@ const Login = promiseMiddleware(async (req: Request, res: Response) => {
|
||||
});
|
||||
}
|
||||
|
||||
if (type === "password" || type === "twofactor") {
|
||||
if (type === "password") {
|
||||
let { username, password, uid } = req.body;
|
||||
|
||||
let user = await User.findOne(username ? { username: username.toLowerCase() } : { uid: uid })
|
||||
@ -61,20 +71,29 @@ const Login = promiseMiddleware(async (req: Request, res: Response) => {
|
||||
if (user.password !== password) {
|
||||
res.json({ error: req.__("Password or username wrong") })
|
||||
} else {
|
||||
if (type === "twofactor") {
|
||||
|
||||
let twofactor = await TwoFactor.find({ user: user._id, valid: true })
|
||||
let expired = twofactor.filter(e => e.expires ? moment().isAfter(moment(e.expires)) : false)
|
||||
await Promise.all(expired.map(e => {
|
||||
e.valid = false;
|
||||
return TwoFactor.save(e);
|
||||
}));
|
||||
twofactor = twofactor.filter(e => e.valid);
|
||||
if (twofactor && twofactor.length > 0) {
|
||||
let tfa = twofactor.map(e => {
|
||||
return {
|
||||
id: e._id,
|
||||
name: e.name,
|
||||
type: e.type
|
||||
}
|
||||
})
|
||||
await sendToken(user, tfa);
|
||||
} else {
|
||||
if (user.twofactor && user.twofactor.length > 0) {
|
||||
let types = user.twofactor.filter(f => f.valid).map(f => f.type)
|
||||
await sendToken(user, types);
|
||||
} else {
|
||||
await sendToken(user);
|
||||
}
|
||||
await sendToken(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new RequestError("Invalid type!", HttpStatusCode.BAD_REQUEST);
|
||||
res.json({ error: req.__("Invalid type!") });
|
||||
}
|
||||
});
|
||||
|
||||
|
29
src/api/user/token.ts
Normal file
29
src/api/user/token.ts
Normal file
@ -0,0 +1,29 @@
|
||||
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 GetToken = Stacker(GetUserMiddleware(true, true), async (req: Request, res: Response) => {
|
||||
let raw_token = await LoginToken.find({ user: req.user._id, valid: true });
|
||||
let token = await Promise.all(raw_token.map(async token => {
|
||||
await CheckToken(token);
|
||||
return {
|
||||
id: token._id,
|
||||
special: token.special,
|
||||
ip: token.ip,
|
||||
browser: token.browser,
|
||||
isthis: token._id.equals(token.special ? req.token.special._id : req.token.login._id)
|
||||
}
|
||||
}).filter(t => t !== undefined));
|
||||
res.json({ token });
|
||||
});
|
||||
|
||||
export const DeleteToken = Stacker(GetUserMiddleware(true, true), async (req: Request, res: Response) => {
|
||||
let { id } = req.query;
|
||||
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;
|
||||
await LoginToken.save(token);
|
||||
res.json({ success: true });
|
||||
});
|
79
src/api/user/twofactor/helper.ts
Normal file
79
src/api/user/twofactor/helper.ts
Normal file
@ -0,0 +1,79 @@
|
||||
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;
|
||||
token.validated = true;
|
||||
//TODO durations from config
|
||||
let expires = (token.special ? moment().add(30, "minute") : moment().add(6, "months")).toDate();
|
||||
token.validTill = expires;
|
||||
await LoginToken.save(token);
|
||||
return expires;
|
||||
}
|
43
src/api/user/twofactor/index.ts
Normal file
43
src/api/user/twofactor/index.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { Router } from "express";
|
||||
import YubiKeyRoute from "./yubikey";
|
||||
import { GetUserMiddleware } from "../../middlewares/user";
|
||||
import Stacker from "../../middlewares/stacker";
|
||||
import TwoFactor from "../../../models/twofactor";
|
||||
import * as moment from "moment"
|
||||
import RequestError, { HttpStatusCode } from "../../../helper/request_error";
|
||||
|
||||
|
||||
const TwoFactorRouter = Router();
|
||||
|
||||
TwoFactorRouter.get("/", Stacker(GetUserMiddleware(true, true), async (req, res) => {
|
||||
let twofactor = await TwoFactor.find({ user: req.user._id, valid: true })
|
||||
let expired = twofactor.filter(e => e.expires ? moment().isAfter(moment(e.expires)) : false)
|
||||
await Promise.all(expired.map(e => {
|
||||
e.valid = false;
|
||||
return TwoFactor.save(e);
|
||||
}));
|
||||
twofactor = twofactor.filter(e => e.valid);
|
||||
let tfa = twofactor.map(e => {
|
||||
return {
|
||||
id: e._id,
|
||||
name: e.name,
|
||||
type: e.type
|
||||
}
|
||||
})
|
||||
res.json({ methods: tfa });
|
||||
}));
|
||||
|
||||
TwoFactorRouter.delete("/", Stacker(GetUserMiddleware(true, true), async (req, res) => {
|
||||
let { id } = req.query;
|
||||
let tfa = await TwoFactor.findById(id);
|
||||
if (!tfa || !tfa.user.equals(req.user._id)) {
|
||||
throw new RequestError("Invalid id", HttpStatusCode.BAD_REQUEST);
|
||||
}
|
||||
tfa.valid = false;
|
||||
await TwoFactor.save(tfa);
|
||||
res.json({ success: true });
|
||||
}));
|
||||
|
||||
TwoFactorRouter.use("/yubikey", YubiKeyRoute);
|
||||
|
||||
export default TwoFactorRouter;
|
131
src/api/user/twofactor/yubikey/index.ts
Normal file
131
src/api/user/twofactor/yubikey/index.ts
Normal file
@ -0,0 +1,131 @@
|
||||
import { Router, Request } from "express"
|
||||
import Stacker from "../../../middlewares/stacker";
|
||||
import { UserMiddleware, GetUserMiddleware } from "../../../middlewares/user";
|
||||
import * as u2f from "u2f";
|
||||
import config from "../../../../config";
|
||||
import TwoFactor, { TFATypes as TwoFATypes, IYubiKey } from "../../../../models/twofactor";
|
||||
import RequestError, { HttpStatusCode } from "../../../../helper/request_error";
|
||||
import moment = require("moment");
|
||||
import LoginToken from "../../../../models/login_token";
|
||||
import { upgradeToken } from "../helper";
|
||||
import Logging from "@hibas123/nodelogging";
|
||||
|
||||
const U2FRoute = Router();
|
||||
|
||||
U2FRoute.post("/", Stacker(GetUserMiddleware(true, true), async (req, res) => {
|
||||
const { type } = req.query;
|
||||
if (type === "challenge") {
|
||||
const registrationRequest = u2f.request(config.core.url);
|
||||
|
||||
let twofactor = TwoFactor.new(<IYubiKey>{
|
||||
user: req.user._id,
|
||||
type: TwoFATypes.U2F,
|
||||
valid: false,
|
||||
data: {
|
||||
registration: registrationRequest
|
||||
}
|
||||
})
|
||||
await TwoFactor.save(twofactor);
|
||||
res.json({ request: registrationRequest, id: twofactor._id, appid: config.core.url });
|
||||
} else {
|
||||
const { response, id } = req.body;
|
||||
Logging.debug(req.body, id);
|
||||
let twofactor: IYubiKey = 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.U2F || !twofactor.data.registration || 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();
|
||||
}
|
||||
|
||||
const result = u2f.checkRegistration(twofactor.data.registration, response);
|
||||
|
||||
if (result.successful) {
|
||||
twofactor.data = {
|
||||
keyHandle: result.keyHandle,
|
||||
publicKey: result.publicKey
|
||||
}
|
||||
twofactor.expires = undefined;
|
||||
twofactor.valid = true;
|
||||
await TwoFactor.save(twofactor);
|
||||
res.json({ success: true });
|
||||
} else {
|
||||
throw new RequestError(result.errorMessage, HttpStatusCode.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
U2FRoute.get("/", Stacker(GetUserMiddleware(true, false, undefined, false), async (req, res) => {
|
||||
let { login, special } = req.token;
|
||||
let twofactor: IYubiKey = await TwoFactor.findOne({ user: req.user._id, type: TwoFATypes.U2F, valid: true })
|
||||
|
||||
if (!twofactor) {
|
||||
throw new RequestError("Invalid Method!", HttpStatusCode.BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (twofactor.expires) {
|
||||
if (moment().isAfter(twofactor.expires)) {
|
||||
twofactor.valid = false;
|
||||
await TwoFactor.save(twofactor);
|
||||
throw new RequestError("Invalid Method!", HttpStatusCode.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
let request = u2f.request(config.core.url, twofactor.data.keyHandle);
|
||||
login.data = {
|
||||
type: "ykr",
|
||||
request
|
||||
};
|
||||
let r;;
|
||||
if (special) {
|
||||
special.data = login.data;
|
||||
r = LoginToken.save(special);
|
||||
}
|
||||
|
||||
await Promise.all([r, LoginToken.save(login)]);
|
||||
res.json({ request });
|
||||
}))
|
||||
|
||||
U2FRoute.put("/", Stacker(GetUserMiddleware(true, false, undefined, false), async (req, res) => {
|
||||
let { login, special } = req.token;
|
||||
let twofactor: IYubiKey = await TwoFactor.findOne({ user: req.user._id, type: TwoFATypes.U2F, valid: true })
|
||||
|
||||
|
||||
let { response } = req.body;
|
||||
if (!twofactor || !login.data || login.data.type !== "ykr" || special && (!special.data || special.data.type !== "ykr")) {
|
||||
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 login_exp;
|
||||
let special_exp;
|
||||
let result = u2f.checkSignature(login.data.request, response, twofactor.data.publicKey);
|
||||
if (result.successful) {
|
||||
if (special) {
|
||||
let result = u2f.checkSignature(special.data.request, response, twofactor.data.publicKey);
|
||||
if (result.successful) {
|
||||
special_exp = await upgradeToken(special);
|
||||
}
|
||||
else {
|
||||
throw new RequestError(result.errorMessage, HttpStatusCode.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
login_exp = await upgradeToken(login);
|
||||
}
|
||||
else {
|
||||
throw new RequestError(result.errorMessage, HttpStatusCode.BAD_REQUEST);
|
||||
}
|
||||
res.json({ success: true, login_exp, special_exp })
|
||||
}))
|
||||
|
||||
export default U2FRoute;
|
@ -10,6 +10,8 @@ export interface WebConfig {
|
||||
|
||||
export interface CoreConfig {
|
||||
name: string
|
||||
url: string
|
||||
dev: string
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
@ -31,11 +33,12 @@ import * as dotenv from "dotenv";
|
||||
import { Logging } from "@hibas123/nodelogging";
|
||||
dotenv.config();
|
||||
|
||||
const config: Config = ini.parse(readFileSync("./config.ini").toString())
|
||||
if (config.dev) config.dev = Boolean(config.dev);
|
||||
if (process.env.DEV === "true") {
|
||||
const config = ini.parse(readFileSync("./config.ini").toString()) as Config;
|
||||
|
||||
if (config.core.dev) config.dev = Boolean(config.core.dev);
|
||||
if (process.env.DEV === "true")
|
||||
config.dev = true;
|
||||
if (config.dev)
|
||||
Logging.warning("DEV mode active. This can cause major performance issues, data loss and vulnerabilities! ")
|
||||
}
|
||||
|
||||
export default config;
|
5
src/express.d.ts
vendored
5
src/express.d.ts
vendored
@ -1,5 +1,6 @@
|
||||
import { IUser } from "./models/user";
|
||||
import { IClient } from "./models/client";
|
||||
import { ILoginToken } from "./models/login_token";
|
||||
|
||||
declare module "express" {
|
||||
interface Request {
|
||||
@ -7,5 +8,9 @@ declare module "express" {
|
||||
client: IClient;
|
||||
isAdmin: boolean;
|
||||
special: boolean;
|
||||
token: {
|
||||
login: ILoginToken;
|
||||
special?: ILoginToken;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import DB from "../database";
|
||||
import { ModelDataBase } from "@hibas123/safe_mongo/lib/model";
|
||||
import { ObjectID } from "mongodb";
|
||||
import moment = require("moment");
|
||||
|
||||
export interface ILoginToken extends ModelDataBase {
|
||||
token: string;
|
||||
@ -9,6 +10,9 @@ export interface ILoginToken extends ModelDataBase {
|
||||
validTill: Date;
|
||||
valid: boolean;
|
||||
validated: boolean;
|
||||
data: any;
|
||||
ip: string;
|
||||
browser: string;
|
||||
}
|
||||
const LoginToken = DB.addModel<ILoginToken>({
|
||||
name: "login_token",
|
||||
@ -31,7 +35,31 @@ const LoginToken = DB.addModel<ILoginToken>({
|
||||
valid: { type: Boolean },
|
||||
validated: { type: Boolean, default: false }
|
||||
}
|
||||
}, {
|
||||
migration: (doc: ILoginToken) => { doc.validated = true; },
|
||||
schema: {
|
||||
token: { type: String },
|
||||
special: { type: Boolean, default: () => false },
|
||||
user: { type: ObjectID },
|
||||
validTill: { type: Date },
|
||||
valid: { type: Boolean },
|
||||
validated: { type: Boolean, default: false },
|
||||
data: { type: "any", optional: true },
|
||||
ip: { type: String, optional: true },
|
||||
browser: { type: String, optional: true }
|
||||
}
|
||||
}]
|
||||
})
|
||||
|
||||
export async function CheckToken(token: ILoginToken, validated: boolean = true): Promise<boolean> {
|
||||
if (!token || !token.valid) return false;
|
||||
if (validated && !token.validated) return false;
|
||||
if (moment().isAfter(token.validTill)) {
|
||||
token.valid = false;
|
||||
await LoginToken.save(token)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export default LoginToken;
|
57
src/models/twofactor.ts
Normal file
57
src/models/twofactor.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import DB from "../database";
|
||||
import { ModelDataBase } from "@hibas123/safe_mongo/lib/model";
|
||||
import { ObjectID } from "bson";
|
||||
|
||||
export enum TFATypes {
|
||||
OTC,
|
||||
BACKUP_CODE,
|
||||
U2F,
|
||||
APP_ALLOW
|
||||
}
|
||||
|
||||
export interface ITwoFactor extends ModelDataBase {
|
||||
user: ObjectID
|
||||
valid: boolean
|
||||
expires?: Date;
|
||||
name?: string;
|
||||
type: TFATypes
|
||||
data: any;
|
||||
}
|
||||
|
||||
export interface IOTP extends ITwoFactor {
|
||||
data: string;
|
||||
}
|
||||
|
||||
export interface IYubiKey extends ITwoFactor {
|
||||
data: {
|
||||
registration?: any;
|
||||
publicKey: string;
|
||||
keyHandle: string;
|
||||
}
|
||||
}
|
||||
|
||||
export interface IU2F extends ITwoFactor {
|
||||
data: {
|
||||
challenge?: string;
|
||||
publicKey: string;
|
||||
keyHandle: string;
|
||||
registration?: string;
|
||||
}
|
||||
}
|
||||
|
||||
const TwoFactor = DB.addModel<ITwoFactor>({
|
||||
name: "twofactor",
|
||||
versions: [{
|
||||
migration: (e) => { },
|
||||
schema: {
|
||||
user: { type: ObjectID },
|
||||
valid: { type: Boolean },
|
||||
expires: { type: Date, optional: true },
|
||||
name: { type: String, optional: true },
|
||||
type: { type: Number },
|
||||
data: { type: "any" },
|
||||
}
|
||||
}]
|
||||
});
|
||||
|
||||
export default TwoFactor;
|
@ -11,11 +11,6 @@ export enum Gender {
|
||||
other
|
||||
}
|
||||
|
||||
export enum TokenTypes {
|
||||
OTC,
|
||||
BACKUP_CODE
|
||||
}
|
||||
|
||||
export interface IUser extends ModelDataBase {
|
||||
uid: string;
|
||||
username: string;
|
||||
@ -28,7 +23,6 @@ export interface IUser extends ModelDataBase {
|
||||
salt: string;
|
||||
mails: ObjectID[];
|
||||
phones: { phone: string, verified: boolean, primary: boolean }[];
|
||||
twofactor: { token: string, valid: boolean, type: TokenTypes }[];
|
||||
encryption_key: string;
|
||||
}
|
||||
|
||||
@ -100,6 +94,33 @@ const User = DB.addModel<IUser>({
|
||||
default: () => randomString(64)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
migration: (e: any) => { delete e.twofactor },
|
||||
schema: {
|
||||
uid: { type: String, default: () => v4() },
|
||||
username: { type: String },
|
||||
name: { type: String },
|
||||
birthday: { type: Date, optional: true },
|
||||
gender: { type: Number },
|
||||
admin: { type: Boolean },
|
||||
password: { type: String },
|
||||
salt: { type: String },
|
||||
mails: { type: Array, default: () => [] },
|
||||
phones: {
|
||||
array: true,
|
||||
model: true,
|
||||
type: {
|
||||
phone: { type: String },
|
||||
verified: { type: Boolean },
|
||||
primary: { type: Boolean }
|
||||
}
|
||||
},
|
||||
encryption_key: {
|
||||
type: String,
|
||||
default: () => randomString(64)
|
||||
}
|
||||
}
|
||||
}]
|
||||
})
|
||||
|
||||
|
@ -6,6 +6,7 @@ import * as moment from "moment";
|
||||
import Permission from "./models/permissions";
|
||||
import { ObjectID } from "bson";
|
||||
import DB from "./database";
|
||||
import TwoFactor from "./models/twofactor";
|
||||
|
||||
export default async function TestData() {
|
||||
await DB.db.dropDatabase();
|
||||
@ -61,4 +62,19 @@ export default async function TestData() {
|
||||
})
|
||||
await RegCode.save(r);
|
||||
}
|
||||
|
||||
let t = await TwoFactor.findOne({ user: u._id, type: 2 })
|
||||
if (!t) {
|
||||
t = TwoFactor.new({
|
||||
user: u._id,
|
||||
type: 2,
|
||||
valid: true,
|
||||
data: {
|
||||
keyHandle: "tWSaMoHX2E96CoZOKOi_4aj6WVEh1e46FKXN0oDY2Z-laNOFcATlStNDo52HX7ygupW-v9qZOCX3J4d5nhOzWQ",
|
||||
publicKey: "BPsgBxR8M7MyrknlFuvYZv0Z1lZxiJQJNrLDA1yi3XKD_lrhIpnAh2OY_TsFjASvn3JTtwlCh62QdMvN-ejQL78"
|
||||
},
|
||||
expires: null
|
||||
})
|
||||
TwoFactor.save(t);
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@ function loadStatic() {
|
||||
let html = readFileSync("./views/out/login/login.html").toString();
|
||||
template = handlebars.compile(html);
|
||||
}
|
||||
|
||||
/**
|
||||
* Benchmarks (5000, 500 cuncurrent)
|
||||
* Plain:
|
||||
@ -26,7 +27,6 @@ function loadStatic() {
|
||||
* - prod 6sec
|
||||
*/
|
||||
|
||||
|
||||
export default function GetLoginPage(__: typeof i__): string {
|
||||
if (config.dev) {
|
||||
loadStatic()
|
||||
|
@ -13,10 +13,11 @@ import Client from "../models/client";
|
||||
import { Logging } from "@hibas123/nodelogging";
|
||||
import Stacker from "../api/middlewares/stacker";
|
||||
import { UserMiddleware, GetUserMiddleware } from "../api/middlewares/user";
|
||||
import GetUserPage from "./user";
|
||||
|
||||
Handlebars.registerHelper("appname", () => config.core.name);
|
||||
|
||||
const cacheTime = moment.duration(1, "month").asSeconds();
|
||||
const cacheTime = config.dev ? moment.duration(1, "month").asSeconds() : 10;
|
||||
|
||||
const ViewRouter: IRouter<void> = Router();
|
||||
ViewRouter.get("/", UserMiddleware, (req, res) => {
|
||||
@ -40,6 +41,11 @@ ViewRouter.get("/admin", GetUserMiddleware(false, true), (req: Request, res, nex
|
||||
res.send(GetAdminPage(req.__))
|
||||
})
|
||||
|
||||
ViewRouter.get("/user", Stacker(GetUserMiddleware(false, true), (req, res) => {
|
||||
res.setHeader("Cache-Control", "public, max-age=" + cacheTime);
|
||||
res.send(GetUserPage(req.__));
|
||||
}));
|
||||
|
||||
ViewRouter.get("/auth", Stacker(GetUserMiddleware(false, true), async (req, res) => {
|
||||
let { scope, redirect_uri, state, client_id }: { [key: string]: string } = req.query;
|
||||
const sendError = (type) => {
|
||||
@ -85,7 +91,7 @@ if (config.dev) {
|
||||
res.send(GetAuthPage(req.__, "Test 05265", [
|
||||
{
|
||||
name: "Access Profile",
|
||||
description: "It allows the application to know who you are. Required for all applications.",
|
||||
description: "It allows the application to know who you are. Required for all applications. And a lot of more Text, because why not? This will not stop, till it is multiple lines long and maybe kill the layout, so keep reading as long as you like, but I promise it will get boring after some time. So this should be enougth.",
|
||||
logo: logo
|
||||
},
|
||||
{
|
||||
|
18
src/web.ts
18
src/web.ts
@ -9,7 +9,7 @@ import * as cookieparser from "cookie-parser"
|
||||
|
||||
import * as i18n from "i18n"
|
||||
import * as compression from "compression";
|
||||
import ApiRouter from "./api/api";
|
||||
import ApiRouter from "./api";
|
||||
import ViewRouter from "./views/views";
|
||||
import RequestError, { HttpStatusCode } from "./helper/request_error";
|
||||
|
||||
@ -64,16 +64,14 @@ export default class Web {
|
||||
next()
|
||||
})
|
||||
|
||||
function shouldCompress(req, res) {
|
||||
if (req.headers['x-no-compression']) {
|
||||
// don't compress responses with this request header
|
||||
return false
|
||||
this.server.use(compression({
|
||||
filter: (req, res) => {
|
||||
if (req.headers['x-no-compression']) {
|
||||
return false
|
||||
}
|
||||
return compression.filter(req, res)
|
||||
}
|
||||
|
||||
// fallback to standard filter function
|
||||
return compression.filter(req, res)
|
||||
}
|
||||
this.server.use(compression({ filter: shouldCompress }))
|
||||
}));
|
||||
}
|
||||
|
||||
private registerEndpoints() {
|
||||
|
Reference in New Issue
Block a user