summary refs log tree commit diff
path: root/api/src/routes
diff options
context:
space:
mode:
Diffstat (limited to 'api/src/routes')
-rw-r--r--api/src/routes/auth/login.ts17
-rw-r--r--api/src/routes/auth/mfa/totp.ts49
-rw-r--r--api/src/routes/users/@me/mfa/codes.ts48
-rw-r--r--api/src/routes/users/@me/mfa/totp/disable.ts45
-rw-r--r--api/src/routes/users/@me/mfa/totp/enable.ts51
5 files changed, 209 insertions, 1 deletions
diff --git a/api/src/routes/auth/login.ts b/api/src/routes/auth/login.ts
index 4b18e67d..bcaccb30 100644
--- a/api/src/routes/auth/login.ts
+++ b/api/src/routes/auth/login.ts
@@ -2,6 +2,7 @@ import { Request, Response, Router } from "express";
 import { route, getIpAdress, verifyCaptcha } from "@fosscord/api";
 import bcrypt from "bcrypt";
 import { Config, User, generateToken, adjustEmail, FieldErrors } from "@fosscord/util";
+import crypto from "crypto";
 
 const router: Router = Router();
 export default router;
@@ -45,7 +46,7 @@ router.post("/", route({ body: "LoginSchema" }), async (req: Request, res: Respo
 
 	const user = await User.findOneOrFail({
 		where: [{ phone: login }, { email: login }],
-		select: ["data", "id", "disabled", "deleted", "settings"]
+		select: ["data", "id", "disabled", "deleted", "settings", "totp_secret", "mfa_enabled"]
 	}).catch((e) => {
 		throw FieldErrors({ login: { message: req.t("auth:login.INVALID_LOGIN"), code: "INVALID_LOGIN" } });
 	});
@@ -65,6 +66,20 @@ router.post("/", route({ body: "LoginSchema" }), async (req: Request, res: Respo
 		throw FieldErrors({ password: { message: req.t("auth:login.INVALID_PASSWORD"), code: "INVALID_PASSWORD" } });
 	}
 
+	if (user.mfa_enabled) {
+		// TODO: This is not a discord.com ticket. I'm not sure what it is but I'm lazy
+		const ticket = crypto.randomBytes(40).toString("hex");
+
+		await User.update({ id: user.id }, { totp_last_ticket: ticket });
+
+		return res.json({
+			ticket: ticket,
+			mfa: true,
+			sms: false,	// TODO
+			token: null,
+		})
+	}
+
 	const token = await generateToken(user.id);
 
 	// Notice this will have a different token structure, than discord
diff --git a/api/src/routes/auth/mfa/totp.ts b/api/src/routes/auth/mfa/totp.ts
new file mode 100644
index 00000000..cec6e5ee
--- /dev/null
+++ b/api/src/routes/auth/mfa/totp.ts
@@ -0,0 +1,49 @@
+import { Router, Request, Response } from "express";
+import { route } from "@fosscord/api";
+import { BackupCode, FieldErrors, generateToken, User } from "@fosscord/util";
+import { verifyToken } from "node-2fa";
+import { HTTPError } from "lambert-server";
+const router = Router();
+
+export interface TotpSchema {
+	code: string,
+	ticket: string,
+	gift_code_sku_id?: string | null,
+	login_source?: string | null,
+}
+
+router.post("/", route({ body: "TotpSchema" }), async (req: Request, res: Response) => {
+	const { code, ticket, gift_code_sku_id, login_source } = req.body as TotpSchema;
+
+	const user = await User.findOneOrFail({
+		where: {
+			totp_last_ticket: ticket,
+		},
+		select: [
+			"id",
+			"totp_secret",
+			"settings",
+		],
+	});
+
+	const backup = await BackupCode.findOne({ code: code, expired: false, consumed: false, user: { id: user.id }});
+
+	if (!backup) {
+		const ret = verifyToken(user.totp_secret!, code);
+		if (!ret || ret.delta != 0)
+			throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008);
+	}
+	else {
+		backup.consumed = true;
+		await backup.save();
+	}
+
+	await User.update({ id: user.id }, { totp_last_ticket: "" });
+
+	return res.json({
+		token: await generateToken(user.id),
+		user_settings: user.settings,
+	});
+});
+
+export default router;
diff --git a/api/src/routes/users/@me/mfa/codes.ts b/api/src/routes/users/@me/mfa/codes.ts
new file mode 100644
index 00000000..2a1fb498
--- /dev/null
+++ b/api/src/routes/users/@me/mfa/codes.ts
@@ -0,0 +1,48 @@
+import { Router, Request, Response } from "express";
+import { route } from "@fosscord/api";
+import { BackupCode, FieldErrors, generateMfaBackupCodes, User } from "@fosscord/util";
+import bcrypt from "bcrypt";
+
+const router = Router();
+
+export interface MfaCodesSchema {
+	password: string;
+	regenerate?: boolean;
+}
+
+// TODO: This route is replaced with users/@me/mfa/codes-verification in newer clients
+
+router.post("/", route({ body: "MfaCodesSchema" }), async (req: Request, res: Response) => {
+	const { password, regenerate } = req.body as MfaCodesSchema;
+
+	const user = await User.findOneOrFail({ id: req.user_id }, { select: ["data"] });
+
+	if (!await bcrypt.compare(password, user.data.hash || "")) {
+		throw FieldErrors({ password: { message: req.t("auth:login.INVALID_PASSWORD"), code: "INVALID_PASSWORD" } });
+	}
+
+	var codes: BackupCode[];
+	if (regenerate) {
+		await BackupCode.update(
+			{ user: { id: req.user_id } },
+			{ expired: true }
+		);
+
+		codes = generateMfaBackupCodes(req.user_id);
+		await Promise.all(codes.map(x => x.save()));
+	}
+	else {
+		codes = await BackupCode.find({
+			user: {
+				id: req.user_id,
+			},
+			expired: false,
+		});
+	}
+
+	return res.json({
+		backup_codes: codes.map(x => ({ ...x, expired: undefined })),
+	})
+});
+
+export default router;
diff --git a/api/src/routes/users/@me/mfa/totp/disable.ts b/api/src/routes/users/@me/mfa/totp/disable.ts
new file mode 100644
index 00000000..5e039ea3
--- /dev/null
+++ b/api/src/routes/users/@me/mfa/totp/disable.ts
@@ -0,0 +1,45 @@
+import { Router, Request, Response } from "express";
+import { route } from "@fosscord/api";
+import { verifyToken } from 'node-2fa';
+import { HTTPError } from "lambert-server";
+import { User, generateToken, BackupCode } from "@fosscord/util";
+
+const router = Router();
+
+export interface TotpDisableSchema {
+	code: string;
+}
+
+router.post("/", route({ body: "TotpDisableSchema" }), async (req: Request, res: Response) => {
+	const body = req.body as TotpDisableSchema;
+
+	const user = await User.findOneOrFail({ id: req.user_id }, { select: ["totp_secret"] });
+
+	const backup = await BackupCode.findOne({ code: body.code });
+	if (!backup) {
+		const ret = verifyToken(user.totp_secret!, body.code);
+		if (!ret || ret.delta != 0)
+			throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008);
+	}
+
+	await User.update(
+		{ id: req.user_id },
+		{
+			mfa_enabled: false,
+			totp_secret: "",
+		},
+	);
+
+	await BackupCode.update(
+		{ user: { id: req.user_id } },
+		{
+			expired: true,
+		}
+	);
+
+	return res.json({
+		token: await generateToken(user.id),
+	});
+});
+
+export default router;
\ No newline at end of file
diff --git a/api/src/routes/users/@me/mfa/totp/enable.ts b/api/src/routes/users/@me/mfa/totp/enable.ts
new file mode 100644
index 00000000..bc5f16ad
--- /dev/null
+++ b/api/src/routes/users/@me/mfa/totp/enable.ts
@@ -0,0 +1,51 @@
+import { Router, Request, Response } from "express";
+import { User, generateToken, BackupCode, generateMfaBackupCodes } from "@fosscord/util";
+import { route } from "@fosscord/api";
+import bcrypt from "bcrypt";
+import { HTTPError } from "lambert-server";
+import { verifyToken } from 'node-2fa';
+import crypto from "crypto";
+
+const router = Router();
+
+export interface TotpEnableSchema {
+	password: string;
+	code?: string;
+	secret?: string;
+}
+
+router.post("/", route({ body: "TotpEnableSchema" }), async (req: Request, res: Response) => {
+	const body = req.body as TotpEnableSchema;
+
+	const user = await User.findOneOrFail({ where: { id: req.user_id }, select: ["data"] });
+
+	// TODO: Are guests allowed to enable 2fa?
+	if (user.data.hash) {
+		if (!await bcrypt.compare(body.password, user.data.hash)) {
+			throw new HTTPError(req.t("auth:login.INVALID_PASSWORD"));
+		}
+	}
+
+	if (!body.secret)
+		throw new HTTPError(req.t("auth:login.INVALID_TOTP_SECRET"), 60005);
+
+	if (!body.code)
+		throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008);
+
+	if (verifyToken(body.secret, body.code)?.delta != 0)
+		throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008);
+
+	let backup_codes = generateMfaBackupCodes(req.user_id);
+	await Promise.all(backup_codes.map(x => x.save()));
+	await User.update(
+		{ id: req.user_id },
+		{ mfa_enabled: true, totp_secret: body.secret }
+	);
+
+	res.send({
+		token: await generateToken(user.id),
+		backup_codes: backup_codes.map(x => ({ ...x, expired: undefined })),
+	});
+});
+
+export default router;
\ No newline at end of file