From c44f0d265a01bc6dbe75c55a21c426b406a9497e Mon Sep 17 00:00:00 2001 From: JJ-8 <2482444-JJ-8@users.noreply.gitlab.com> Date: Sat, 3 Apr 2021 17:17:16 +0200 Subject: [PATCH] Add Hedgedoc authentication link When a new account is created, CTFNote now tries to register the user to Hedgedoc. After that, the user will be automatically authenticated to Hedgedoc when logging in. The slug and password of the user are used for registering. Only new accounts are registered to Hedgedoc. However, on every log in CTFNote tries to authenticate the user to Hedgedoc. This does not cause an error. The benefit of authenticating is that the cursor of the user shows the username/slug and changes by users are shown by Hedgedoc. The slug is used instead of the username because the slug never changes for a user. If the username was used and it changes, then authentication will always fail on login. This feature can be disabled by the env variable MD_AUTH. --- api/src/config/globals.ts | 4 ++ .../auth.controller/login.action.ts | 7 +++ .../auth.controller/logout.action.ts | 1 + .../auth.controller/register.action.ts | 16 +++++- .../config.controller/update.action.ts | 2 + api/src/services/pad.ts | 56 +++++++++++++++++++ 6 files changed, 84 insertions(+), 2 deletions(-) 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); + } }