From fbfe8c63edcfd08bb45f1ff52df5e737b9ce1f78 Mon Sep 17 00:00:00 2001 From: Fabian Stamm Date: Tue, 9 Jun 2020 14:20:38 +0200 Subject: [PATCH] First steps towards session support --- .drone.yml | 3 - src/api/middlewares/user.ts | 14 +- src/api/oauth/auth.ts | 4 +- src/api/user/twofactor/otc/index.ts | 205 ++++++++++++++++------------ src/database.ts | 18 ++- src/express.d.ts | 5 +- src/views/views.ts | 114 +++++++++++----- views/src/admin/admin.js | 160 +++++++++++++--------- 8 files changed, 322 insertions(+), 201 deletions(-) diff --git a/.drone.yml b/.drone.yml index 4cbd902..3953734 100644 --- a/.drone.yml +++ b/.drone.yml @@ -22,6 +22,3 @@ steps: debug: true when: branch: master - event: - exclude: - - pull_request diff --git a/src/api/middlewares/user.ts b/src/api/middlewares/user.ts index 9c4e5bc..796ffb5 100644 --- a/src/api/middlewares/user.ts +++ b/src/api/middlewares/user.ts @@ -1,5 +1,5 @@ import { NextFunction, Request, Response } from "express"; -import LoginToken, { CheckToken } from "../../models/login_token"; +import LoginToken, { CheckToken, ILoginToken } from "../../models/login_token"; import Logging from "@hibas123/nodelogging"; import RequestError, { HttpStatusCode } from "../../helper/request_error"; import User from "../../models/user"; @@ -22,7 +22,7 @@ export function GetUserMiddleware( redirect_uri?: string, validated = true ) { - return promiseMiddleware(async function( + return promiseMiddleware(async function ( req: Request, res: Response, next?: NextFunction @@ -50,25 +50,29 @@ export function GetUserMiddleware( invalid("Login token invalid"); } - let special_token; + let special_token: ILoginToken; + let session: any; if (special) { Logging.debug("Special found"); special_token = await LoginToken.findOne({ token: special, special: true, valid: true, - user: token.user + user: token.user, }); if (!(await CheckToken(special_token, validated))) invalid("Special token invalid"); req.special = true; + + // } req.user = user; req.isAdmin = user.admin; req.token = { login: token, - special: special_token + special: special_token, + session, }; if (next) next(); diff --git a/src/api/oauth/auth.ts b/src/api/oauth/auth.ts index d8370f0..52e9ec7 100644 --- a/src/api/oauth/auth.ts +++ b/src/api/oauth/auth.ts @@ -97,7 +97,7 @@ const GetAuthRoute = (view = false) => } = req.query; const sendError = type => { if (redirect_uri === "$local") redirect_uri = "/code"; - res.redirect((redirect_uri += `?error=${type}${state ? "&state=" + state : ""}`)); + res.redirect((redirect_uri += `?error=${type}&state=${state}`)); }; const scopes = scope.split(";"); @@ -228,7 +228,7 @@ const GetAuthRoute = (view = false) => let redir = client.redirect_url === "$local" ? "/code" : client.redirect_url; - let ruri = redir + `?code=${code.code}${state ? "&state=" + state : ""}`; + let ruri = redir + `?code=${code.code}&state=${state}`; if (nored === "true") { res.json({ redirect_uri: ruri diff --git a/src/api/user/twofactor/otc/index.ts b/src/api/user/twofactor/otc/index.ts index ad85ac7..ceb69b7 100644 --- a/src/api/user/twofactor/otc/index.ts +++ b/src/api/user/twofactor/otc/index.ts @@ -1,7 +1,10 @@ -import { Router } from "express" +import { Router } from "express"; import Stacker from "../../../middlewares/stacker"; import { GetUserMiddleware } from "../../../middlewares/user"; -import TwoFactor, { TFATypes as TwoFATypes, IOTC } from "../../../../models/twofactor"; +import TwoFactor, { + TFATypes as TwoFATypes, + IOTC, +} from "../../../../models/twofactor"; import RequestError, { HttpStatusCode } from "../../../../helper/request_error"; import moment = require("moment"); import { upgradeToken } from "../helper"; @@ -13,93 +16,125 @@ 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({ - 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; +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({ + 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({ success: true }); + 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 Code!", HttpStatusCode.BAD_REQUEST); + throw new RequestError("Invalid type", 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); - } -})) +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 && config.core.dev === true && code === "000000") { + Logging.warning("Dev mode enabled and dev OTC used."); + valid = true; + } + + 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; diff --git a/src/database.ts b/src/database.ts index b65a9eb..78b7c92 100644 --- a/src/database.ts +++ b/src/database.ts @@ -1,11 +1,19 @@ import SafeMongo from "@hibas123/safe_mongo"; -import Config from "./config" -let dbname = "openauth" -let host = "localhost" +import Config from "./config"; + +let dbname = "openauth"; +let host = "localhost"; + if (Config.database) { if (Config.database.database) dbname = Config.database.database; if (Config.database.host) host = Config.database.host; } + if (Config.core.dev) dbname += "_dev"; -const DB = new SafeMongo("mongodb://" + host, dbname); -export default DB; \ No newline at end of file + +const DB = new SafeMongo("mongodb://" + host, dbname, { + useUnifiedTopology: true, + useNewUrlParser: true, +} as any); + +export default DB; diff --git a/src/express.d.ts b/src/express.d.ts index 808fecd..7cd6dbe 100644 --- a/src/express.d.ts +++ b/src/express.d.ts @@ -11,6 +11,7 @@ declare module "express" { token: { login: ILoginToken; special?: ILoginToken; - } + session?: { [key: string]: any }; + }; } -} \ No newline at end of file +} diff --git a/src/views/views.ts b/src/views/views.ts index 1f16e96..f808679 100644 --- a/src/views/views.ts +++ b/src/views/views.ts @@ -1,30 +1,25 @@ -import { - IRouter, - Request, - Router, - static as ServeStatic, - RequestHandler, -} from "express"; -import * as Handlebars from "handlebars"; -import * as moment from "moment"; -import { GetUserMiddleware, UserMiddleware } from "../api/middlewares/user"; -import GetAuthRoute from "../api/oauth/auth"; -import config from "../config"; -import { HttpStatusCode } from "../helper/request_error"; -import GetAdminPage from "./admin"; +import { Router, IRouter, Request, static as ServeStatic } from "express"; +import GetLoginPage from "./login"; import GetAuthPage from "./authorize"; +import promiseMiddleware from "../helper/promiseMiddleware"; +import config from "../config"; +import * as Handlebars from "handlebars"; import GetRegistrationPage from "./register"; +import GetAdminPage from "./admin"; +import { HttpStatusCode } from "../helper/request_error"; +import * as moment from "moment"; +import Permission, { IPermission } from "../models/permissions"; +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 = !config.core.dev +const cacheTime = config.core.dev ? moment.duration(1, "month").asSeconds() - : 1000; - -const addCache: RequestHandler = (req, res, next) => { - res.setHeader("cache-control", "public, max-age=" + cacheTime); - next(); -}; + : 10; const ViewRouter: IRouter = Router(); ViewRouter.get("/", UserMiddleware, (req, res) => { @@ -36,17 +31,9 @@ ViewRouter.get("/register", (req, res) => { res.send(GetRegistrationPage(req.__)); }); -ViewRouter.use( - "/login", - addCache, - ServeStatic("./views_repo/build/login", { cacheControl: false }) -); +ViewRouter.use("/login", ServeStatic("./views_repo/build/login")); -ViewRouter.use( - "/user", - addCache, - ServeStatic("./views_repo/build/user", { cacheControl: false }) -); +ViewRouter.use("/user", ServeStatic("./views_repo/build/user")); ViewRouter.get("/code", (req, res) => { res.setHeader("Cache-Control", "no-cache"); @@ -66,7 +53,62 @@ ViewRouter.get( } ); -ViewRouter.get("/auth", GetAuthRoute(true)); + +import GetAuthRoute from "../api/oauth/auth"; + +ViewRouter.get("/auth", GetAuthRoute(true)) + +// 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 => { +// res.redirect((redirect_uri += `?error=${type}&state=${state}`)); +// }; + +// let client = await Client.findOne({ client_id: client_id }); +// if (!client) { +// return sendError("unauthorized_client"); +// } + +// let permissions: IPermission[] = []; +// let proms: PromiseLike[] = []; +// if (scope) { +// for (let perm of scope.split(";").filter(e => e !== "read_user")) { +// proms.push( +// Permission.findById(perm).then(p => { +// if (!p) return Promise.reject(new Error()); +// permissions.push(p); +// }) +// ); +// } +// } +// let err = false; +// await Promise.all(proms).catch(e => { +// err = true; +// }); +// Logging.debug(err); +// if (err) { +// return sendError("invalid_scope"); +// } +// let scopes = await Promise.all( +// permissions.map(async perm => { +// let client = await Client.findById(perm.client); +// return { +// name: perm.name, +// description: perm.description, +// logo: client.logo +// }; +// }) +// ); +// res.send(GetAuthPage(req.__, client.name, scopes)); +// }) +// ); if (config.core.dev) { const logo = @@ -78,20 +120,20 @@ if (config.core.dev) { name: "Access Profile", 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, + logo: logo }, { name: "Test 1", description: "This is not an real permission. This is used just to verify the layout", - logo: logo, + logo: logo }, { name: "Test 2", description: "This is not an real permission. This is used just to verify the layout", - logo: logo, - }, + logo: logo + } ]) ); }); diff --git a/views/src/admin/admin.js b/views/src/admin/admin.js index 619b2a3..e8a0ea6 100644 --- a/views/src/admin/admin.js +++ b/views/src/admin/admin.js @@ -1,6 +1,6 @@ import request from "../../shared/request"; import getFormData from "../../shared/formdata"; -Handlebars.registerHelper("humangender", function (value, options) { +Handlebars.registerHelper("humangender", function(value, options) { switch (value) { case 1: return "male"; @@ -14,8 +14,8 @@ Handlebars.registerHelper("humangender", function (value, options) { } }); -// Deprecated since version 0.8.0 -Handlebars.registerHelper("formatDate", function (datetime, format) { +// Deprecated since version 0.8.0 +Handlebars.registerHelper("formatDate", function(datetime, format) { return new Date(datetime).toLocaleString(); }); @@ -26,9 +26,8 @@ Handlebars.registerHelper("formatDate", function (datetime, format) { document.getElementById("sitename").innerText = title; } - - const cc = document.getElementById("custom_data") - const ccc = document.getElementById("custom_data_cont") + const cc = document.getElementById("custom_data"); + const ccc = document.getElementById("custom_data_cont"); function setCustomCard(content) { if (!content) { @@ -40,8 +39,8 @@ Handlebars.registerHelper("formatDate", function (datetime, format) { } } - const error_cont = document.getElementById("error_cont") - const error_msg = document.getElementById("error_msg") + const error_cont = document.getElementById("error_cont"); + const error_msg = document.getElementById("error_msg"); function catchError(error) { error_cont.style.display = ""; @@ -50,40 +49,53 @@ Handlebars.registerHelper("formatDate", function (datetime, format) { } async function renderUser() { - console.log("Rendering User") - setTitle("User") - const listt = Handlebars.compile(document.getElementById("template-user-list").innerText) + console.log("Rendering User"); + setTitle("User"); + const listt = Handlebars.compile( + document.getElementById("template-user-list").innerText + ); async function loadList() { let data = await request("/api/admin/user", "GET"); tableb.innerHTML = listt({ users: data - }) + }); } - window.userOnChangeType = (id) => { - request("/api/admin/user?id=" + id, "PUT").then(() => loadList()).catch(catchError) - } + window.userOnChangeType = id => { + request("/api/admin/user?id=" + id, "PUT") + .then(() => loadList()) + .catch(catchError); + }; - window.deleteUser = (id) => { - request("/api/admin/user?id=" + id, "DELETE").then(() => loadList()).catch(catchError) - } + window.deleteUser = id => { + request("/api/admin/user?id=" + id, "DELETE") + .then(() => loadList()) + .catch(catchError); + }; await loadList(); } async function renderPermissions(client_id, client_name) { - const listt = Handlebars.compile(document.getElementById("template-permission-list").innerText); - const formt = Handlebars.compile(document.getElementById("template-permission-form").innerText); + const listt = Handlebars.compile( + document.getElementById("template-permission-list").innerText + ); + const formt = Handlebars.compile( + document.getElementById("template-permission-form").innerText + ); setCustomCard(); async function loadList() { try { - let data = await request("/api/admin/permission?client=" + client_id, "GET"); + let data = await request( + "/api/admin/permission?client=" + client_id, + "GET" + ); tableb.innerHTML = listt({ client_id: client_id, client_name: client_name, permissions: data - }) + }); } catch (err) { catchError(err); } @@ -91,11 +103,13 @@ Handlebars.registerHelper("formatDate", function (datetime, format) { window.gotoClients = () => { renderClient(); - } + }; - window.deletePermission = (id) => { - request("/api/admin/permission?id=" + id, "DELETE").then(() => loadList()).catch(catchError) - } + window.deletePermission = id => { + request("/api/admin/permission?id=" + id, "DELETE") + .then(() => loadList()) + .catch(catchError); + }; window.createPermission = () => { try { @@ -103,24 +117,30 @@ Handlebars.registerHelper("formatDate", function (datetime, format) { } catch (err) { console.log("Err", err); } - } + }; - - window.createPermissionSubmit = (elm) => { + window.createPermissionSubmit = elm => { console.log(elm); let data = getFormData(elm); console.log(data); - request("/api/admin/permission", "POST", data).then(() => setCustomCard()).then(() => loadList()).catch(catchError) - } - await loadList() + request("/api/admin/permission", "POST", data) + .then(() => setCustomCard()) + .then(() => loadList()) + .catch(catchError); + }; + await loadList(); } async function renderClient() { - console.log("Rendering Client") - setTitle("Client") + console.log("Rendering Client"); + setTitle("Client"); - const listt = Handlebars.compile(document.getElementById("template-client-list").innerText) - const formt = Handlebars.compile(document.getElementById("template-client-form").innerText) + const listt = Handlebars.compile( + document.getElementById("template-client-list").innerText + ); + const formt = Handlebars.compile( + document.getElementById("template-client-form").innerText + ); let clients = []; async function loadList() { @@ -128,63 +148,77 @@ Handlebars.registerHelper("formatDate", function (datetime, format) { clients = data; tableb.innerHTML = listt({ clients: data - }) + }); } - window.permissionsClient = (id) => { + window.permissionsClient = id => { renderPermissions(id, clients.find(e => e._id === id).name); - } + }; - window.deleteClient = (id) => { - request("/api/admin/client/id=" + id, "DELETE").then(() => loadList()).catch(catchError) - } + window.deleteClient = id => { + request("/api/admin/client?id=" + id, "DELETE") + .then(() => loadList()) + .catch(catchError); + }; - window.createClientSubmit = (elm) => { + window.createClientSubmit = elm => { console.log(elm); let data = getFormData(elm); console.log(data); let id = data.id; delete data.id; if (id && id !== "") { - request("/api/admin/client?id=" + id, "PUT", data).then(() => setCustomCard()).then(() => loadList()).catch(catchError) + request("/api/admin/client?id=" + id, "PUT", data) + .then(() => setCustomCard()) + .then(() => loadList()) + .catch(catchError); } else { - request("/api/admin/client", "POST", data).then(() => setCustomCard()).then(() => loadList()).catch(catchError) + request("/api/admin/client", "POST", data) + .then(() => setCustomCard()) + .then(() => loadList()) + .catch(catchError); } - } + }; window.createClient = () => { setCustomCard(formt()); - } + }; - window.editClient = (id) => { + window.editClient = id => { let client = clients.find(e => e._id === id); - if (!client) return catchError(new Error("Client does not exist!!")) + if (!client) return catchError(new Error("Client does not exist!!")); setCustomCard(formt(client)); - } + }; await loadList().catch(catchError); } async function renderRegCode() { - console.log("Rendering RegCode") - setTitle("RegCode") + console.log("Rendering RegCode"); + setTitle("RegCode"); - const listt = Handlebars.compile(document.getElementById("template-regcode-list").innerText) + const listt = Handlebars.compile( + document.getElementById("template-regcode-list").innerText + ); async function loadList() { let data = await request("/api/admin/regcode", "GET"); tableb.innerHTML = listt({ regcodes: data - }) + }); } - window.deleteRegcode = (id) => { - request("/api/admin/regcode?id=" + id, "DELETE").then(() => loadList()).catch(catchError) - } + window.deleteRegcode = id => { + request("/api/admin/regcode?id=" + id, "DELETE") + .then(() => loadList()) + .catch(catchError); + }; window.createRegcode = () => { - request("/api/admin/regcode", "POST").then(() => loadList()).catch(catchError); - } + request("/api/admin/regcode", "POST") + .then(() => loadList()) + .catch(catchError); + }; await loadList().catch(catchError); } @@ -192,14 +226,14 @@ Handlebars.registerHelper("formatDate", function (datetime, format) { const type = new URL(window.location.href).searchParams.get("type"); switch (type) { case "client": - renderClient().catch(catchError) - break + renderClient().catch(catchError); + break; case "regcode": - renderRegCode().catch(catchError) + renderRegCode().catch(catchError); break; case "user": default: renderUser().catch(catchError); break; } -})() \ No newline at end of file +})();