diff --git a/api/src/config/globals.ts b/api/src/config/globals.ts index 07369f479..729c51a7c 100644 --- a/api/src/config/globals.ts +++ b/api/src/config/globals.ts @@ -15,6 +15,9 @@ export default class Globals { static cookieName = "ctfnote-auth"; static userAgent = "CTFNote"; + static hedgedocCookieName = "connect.sid"; + static hedgedocAuth = process.env.MD_AUTH == "true" || true; + static adminRights = [Rights.ADMIN_ALL]; static defaultRights = []; @@ -36,5 +39,6 @@ export default class Globals { "allow-registration", Globals.allowRegistration ); + Globals.hedgedocAuth = await PersistentConfiguration.setIfNotSet("hedgedoc-auth", Globals.hedgedocAuth); } } diff --git a/api/src/controllers/auth.controller/login.action.ts b/api/src/controllers/auth.controller/login.action.ts index 2b2e4d4f7..9dfea5f2b 100644 --- a/api/src/controllers/auth.controller/login.action.ts +++ b/api/src/controllers/auth.controller/login.action.ts @@ -11,6 +11,7 @@ import PasswordUtil from "../../utils/password"; import authLimiter from "../../ratelimits/auth"; import { getConnection } from "typeorm"; import SessionManager from "../../utils/session"; +import PadService from "../../services/pad"; function deny(res: Response) { return res.status(403).json({ errors: [{ msg: `Invalid username/password` }] }); @@ -44,6 +45,12 @@ const LoginAction: IRoute = { if (!session) return deny(res); + //try to authenticate with HedgeDoc + let padCookie = await PadService.login(username, password); + if (padCookie != null) { + res.setHeader("Set-Cookie", padCookie); + } + res.cookie(Globals.cookieName, session.uuid, { expires: session.expiresAt, httpOnly: true, diff --git a/api/src/controllers/auth.controller/logout.action.ts b/api/src/controllers/auth.controller/logout.action.ts index 0c6ad133b..6d7c595bc 100644 --- a/api/src/controllers/auth.controller/logout.action.ts +++ b/api/src/controllers/auth.controller/logout.action.ts @@ -12,6 +12,7 @@ const LogoutAction: IRoute = { if (logout) await SessionManager.invalidateSession(req.cookies[Globals.cookieName]); + res.clearCookie(Globals.hedgedocCookieName); return res.status(204).send(); }, }; diff --git a/api/src/controllers/auth.controller/register.action.ts b/api/src/controllers/auth.controller/register.action.ts index 9072d598b..fef0f9192 100644 --- a/api/src/controllers/auth.controller/register.action.ts +++ b/api/src/controllers/auth.controller/register.action.ts @@ -11,8 +11,8 @@ import authLimiter from "../../ratelimits/auth"; import makeSlug from "../../utils/slugify"; import SessionManager from "../../utils/session"; -import Rights from "../../config/rights"; import PersistentConfiguration from "../../config/persitent"; +import PadService from "../../services/pad"; const RegisterAction: IRoute = { middlewares: [ @@ -42,7 +42,12 @@ const RegisterAction: IRoute = { const hash = await PasswordUtil.hash(password); const firstAccount = (await userRepo.count()) === 0; - let user = userRepo.create({ username, slug, password: hash, rights: firstAccount ? Globals.adminRights : Globals.defaultRights }); + let user = userRepo.create({ + username, + slug, + password: hash, + rights: firstAccount ? Globals.adminRights : Globals.defaultRights, + }); try { user = await userRepo.save(user); @@ -50,6 +55,13 @@ const RegisterAction: IRoute = { return res.status(409).json({ errors: [{ msg: "A user with that username already exists" }] }); } + //try to authenticate with HedgeDoc + let padCookie = await PadService.register(username, req.body.password); + if (padCookie != null) { + padCookie = await PadService.login(username, req.body.password); + res.setHeader("Set-Cookie", padCookie); + } + const session = await SessionManager.generateSession(user.slug); res.cookie(Globals.cookieName, session.uuid, { expires: session.expiresAt, diff --git a/api/src/controllers/config.controller/update.action.ts b/api/src/controllers/config.controller/update.action.ts index 9ea670471..20be196b4 100644 --- a/api/src/controllers/config.controller/update.action.ts +++ b/api/src/controllers/config.controller/update.action.ts @@ -19,11 +19,13 @@ const UpdateConfigAction: IRoute = { "md-create-url": mdCreateUrl, "md-show-url": mdShowUrl, "allow-registration": allowRegistration, + "hedgedoc-auth": hedgedocAuth, } = req.body; if (mdCreateUrl != null) await PersistentConfiguration.set("md-create-url", mdCreateUrl); if (mdShowUrl != null) await PersistentConfiguration.set("md-show-url", mdShowUrl); if (allowRegistration != null) await PersistentConfiguration.set("allow-registration", allowRegistration); + if (hedgedocAuth != null) await PersistentConfiguration.set("hedgedoc-auth", hedgedocAuth); return res.status(200).json(await PersistentConfiguration.list()); }, diff --git a/api/src/services/pad.ts b/api/src/services/pad.ts index 9baddd840..b960f14eb 100644 --- a/api/src/services/pad.ts +++ b/api/src/services/pad.ts @@ -1,6 +1,8 @@ import Axios from "axios"; import logger from "../config/logger"; import PersistentConfiguration from "../config/persitent"; +import querystring from "querystring"; +import Globals from "../config/globals"; export default class PadService { /** @@ -22,4 +24,58 @@ export default class PadService { return "#"; } } + + private static async baseUrl(): Promise { + let cleanUrl: string = await PersistentConfiguration.get("md-create-url"); + cleanUrl = cleanUrl.slice(0, -4); //remove '/new' for clean url + return cleanUrl; + } + + private static async authPad(username: string, password: string, url: URL): Promise { + if (!Globals.hedgedocAuth) { + return null; + } + + let domain: string; + //if domain does not end in '.[tld]', it will be rejected + //so we add '.local' manually + if (url.hostname.split(".").length == 1) { + domain = `${url.hostname}.local`; + } else { + domain = url.hostname; + } + + const email = `${username}@${domain}`; + + try { + const res = await Axios.post( + url.toString(), + querystring.stringify({ + email: email, + password: password, + }), + { + validateStatus: (status) => status === 302, + maxRedirects: 0, + timeout: 5000, + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + } + ); + return res.headers["set-cookie"]; + } catch (e) { + logger.warn(`Could not auth to pad service: '${url.toString()}': `); + logger.fatal(e); + return null; + } + } + + static async register(username: string, password: string): Promise { + const authUrl = new URL(`${await this.baseUrl()}/register`); + return this.authPad(username, password, authUrl); + } + + static async login(username: string, password: string): Promise { + const authUrl = new URL(`${await this.baseUrl()}/login`); + return this.authPad(username, password, authUrl); + } }