summary refs log tree commit diff
path: root/src/api/routes/auth
diff options
context:
space:
mode:
Diffstat (limited to 'src/api/routes/auth')
-rw-r--r--src/api/routes/auth/generate-registration-tokens.ts49
-rw-r--r--src/api/routes/auth/location-metadata.ts17
-rw-r--r--src/api/routes/auth/login.ts138
-rw-r--r--src/api/routes/auth/logout.ts17
-rw-r--r--src/api/routes/auth/mfa/totp.ts52
-rw-r--r--src/api/routes/auth/register.ts278
-rw-r--r--src/api/routes/auth/verify/view-backup-codes-challenge.ts34
7 files changed, 0 insertions, 585 deletions
diff --git a/src/api/routes/auth/generate-registration-tokens.ts b/src/api/routes/auth/generate-registration-tokens.ts
deleted file mode 100644

index ba40bd9a..00000000 --- a/src/api/routes/auth/generate-registration-tokens.ts +++ /dev/null
@@ -1,49 +0,0 @@ -import { route, random } from "@fosscord/api"; -import { Config, ValidRegistrationToken } from "@fosscord/util"; -import { Request, Response, Router } from "express"; - -const router: Router = Router(); -export default router; - -router.get( - "/", - route({ right: "OPERATOR" }), - async (req: Request, res: Response) => { - const count = req.query.count ? parseInt(req.query.count as string) : 1; - const length = req.query.length - ? parseInt(req.query.length as string) - : 255; - - let tokens: ValidRegistrationToken[] = []; - - for (let i = 0; i < count; i++) { - const token = ValidRegistrationToken.create({ - token: random(length), - expires_at: - Date.now() + - Config.get().security.defaultRegistrationTokenExpiration, - }); - tokens.push(token); - } - - // Why are these options used, exactly? - await ValidRegistrationToken.save(tokens, { - chunk: 1000, - reload: false, - transaction: false, - }); - - const ret = req.query.include_url - ? tokens.map( - (x) => - `${Config.get().general.frontPage}/register?token=${ - x.token - }`, - ) - : tokens.map((x) => x.token); - - if (req.query.plain) return res.send(ret.join("\n")); - - return res.json({ tokens: ret }); - }, -); diff --git a/src/api/routes/auth/location-metadata.ts b/src/api/routes/auth/location-metadata.ts deleted file mode 100644
index 0ae946ed..00000000 --- a/src/api/routes/auth/location-metadata.ts +++ /dev/null
@@ -1,17 +0,0 @@ -import { Router, Request, Response } from "express"; -import { route } from "@fosscord/api"; -import { getIpAdress, IPAnalysis } from "@fosscord/api"; -const router = Router(); - -router.get("/", route({}), async (req: Request, res: Response) => { - //TODO - //Note: It's most likely related to legal. At the moment Discord hasn't finished this too - const country_code = (await IPAnalysis(getIpAdress(req))).country_code; - res.json({ - consent_required: false, - country_code: country_code, - promotional_email_opt_in: { required: true, pre_checked: false }, - }); -}); - -export default router; diff --git a/src/api/routes/auth/login.ts b/src/api/routes/auth/login.ts deleted file mode 100644
index 7434fa35..00000000 --- a/src/api/routes/auth/login.ts +++ /dev/null
@@ -1,138 +0,0 @@ -import { Request, Response, Router } from "express"; -import { route, getIpAdress, verifyCaptcha } from "@fosscord/api"; -import bcrypt from "bcrypt"; -import { - Config, - User, - generateToken, - adjustEmail, - FieldErrors, - LoginSchema, -} from "@fosscord/util"; -import crypto from "crypto"; - -const router: Router = Router(); -export default router; - -router.post( - "/", - route({ body: "LoginSchema" }), - async (req: Request, res: Response) => { - const { login, password, captcha_key, undelete } = - req.body as LoginSchema; - const email = adjustEmail(login); - - const config = Config.get(); - - if (config.login.requireCaptcha && config.security.captcha.enabled) { - const { sitekey, service } = config.security.captcha; - if (!captcha_key) { - return res.status(400).json({ - captcha_key: ["captcha-required"], - captcha_sitekey: sitekey, - captcha_service: service, - }); - } - - const ip = getIpAdress(req); - const verify = await verifyCaptcha(captcha_key, ip); - if (!verify.success) { - return res.status(400).json({ - captcha_key: verify["error-codes"], - captcha_sitekey: sitekey, - captcha_service: service, - }); - } - } - - const user = await User.findOneOrFail({ - where: [{ phone: login }, { email: email }], - 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", - }, - }); - }); - - if (undelete) { - // undelete refers to un'disable' here - if (user.disabled) - await User.update({ id: user.id }, { disabled: false }); - if (user.deleted) - await User.update({ id: user.id }, { deleted: false }); - } else { - if (user.deleted) - return res.status(400).json({ - message: "This account is scheduled for deletion.", - code: 20011, - }); - if (user.disabled) - return res.status(400).json({ - message: req.t("auth:login.ACCOUNT_DISABLED"), - code: 20013, - }); - } - - // the salt is saved in the password refer to bcrypt docs - const same_password = await bcrypt.compare( - password, - user.data.hash || "", - ); - if (!same_password) { - 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 - // Discord header is just the user id as string, which is not possible with npm-jsonwebtoken package - // https://user-images.githubusercontent.com/6506416/81051916-dd8c9900-8ec2-11ea-8794-daf12d6f31f0.png - - res.json({ token, settings: user.settings }); - }, -); - -/** - * POST /auth/login - * @argument { login: "email@gmail.com", password: "cleartextpassword", undelete: false, captcha_key: null, login_source: null, gift_code_sku_id: null, } - - * MFA required: - * @returns {"token": null, "mfa": true, "sms": true, "ticket": "SOME TICKET JWT TOKEN"} - - * Captcha required: - * @returns {"captcha_key": ["captcha-required"], "captcha_sitekey": null, "captcha_service": "recaptcha"} - - * Sucess: - * @returns {"token": "USERTOKEN", "settings": {"locale": "en", "theme": "dark"}} - - */ diff --git a/src/api/routes/auth/logout.ts b/src/api/routes/auth/logout.ts deleted file mode 100644
index e1bdbea3..00000000 --- a/src/api/routes/auth/logout.ts +++ /dev/null
@@ -1,17 +0,0 @@ -import { route } from "@fosscord/api"; -import { Request, Response, Router } from "express"; - -const router: Router = Router(); -export default router; - -router.post("/", route({}), async (req: Request, res: Response) => { - if (req.body.provider != null || req.body.voip_provider != null) { - console.log(`[LOGOUT]: provider or voip provider not null!`, req.body); - } else { - delete req.body.provider; - delete req.body.voip_provider; - if (Object.keys(req.body).length != 0) - console.log(`[LOGOUT]: Extra fields sent in logout!`, req.body); - } - res.status(204).send(); -}); diff --git a/src/api/routes/auth/mfa/totp.ts b/src/api/routes/auth/mfa/totp.ts deleted file mode 100644
index 83cf7648..00000000 --- a/src/api/routes/auth/mfa/totp.ts +++ /dev/null
@@ -1,52 +0,0 @@ -import { Router, Request, Response } from "express"; -import { route } from "@fosscord/api"; -import { BackupCode, generateToken, User, TotpSchema } from "@fosscord/util"; -import { verifyToken } from "node-2fa"; -import { HTTPError } from "lambert-server"; -const router = Router(); - -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({ - where: { - 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/src/api/routes/auth/register.ts b/src/api/routes/auth/register.ts deleted file mode 100644
index 3d968114..00000000 --- a/src/api/routes/auth/register.ts +++ /dev/null
@@ -1,278 +0,0 @@ -import { Request, Response, Router } from "express"; -import { - Config, - generateToken, - Invite, - FieldErrors, - User, - adjustEmail, - RegisterSchema, - ValidRegistrationToken, -} from "@fosscord/util"; -import { - route, - getIpAdress, - IPAnalysis, - isProxy, - verifyCaptcha, -} from "@fosscord/api"; -import bcrypt from "bcrypt"; -import { HTTPError } from "lambert-server"; -import { LessThan, MoreThan } from "typeorm"; - -const router: Router = Router(); - -router.post( - "/", - route({ body: "RegisterSchema" }), - async (req: Request, res: Response) => { - const body = req.body as RegisterSchema; - const { register, security, limits } = Config.get(); - const ip = getIpAdress(req); - - // Reg tokens - // They're a one time use token that bypasses registration limits ( rates, disabled reg, etc ) - let regTokenUsed = false; - if (req.get("Referrer") && req.get("Referrer")?.includes("token=")) { - // eg theyre on https://staging.fosscord.com/register?token=whatever - const token = req.get("Referrer")!.split("token=")[1].split("&")[0]; - if (token) { - const regToken = await ValidRegistrationToken.findOne({ - where: { token, expires_at: MoreThan(new Date()) }, - }); - await ValidRegistrationToken.delete({ token }); - regTokenUsed = true; - console.log( - `[REGISTER] Registration token ${token} used for registration!`, - ); - } else { - console.log( - `[REGISTER] Invalid registration token ${token} used for registration by ${ip}!`, - ); - } - } - - // email will be slightly modified version of the user supplied email -> e.g. protection against GMail Trick - let email = adjustEmail(body.email); - - // check if registration is allowed - if (!regTokenUsed && !register.allowNewRegistration) { - throw FieldErrors({ - email: { - code: "REGISTRATION_DISABLED", - message: req.t("auth:register.REGISTRATION_DISABLED"), - }, - }); - } - - // check if the user agreed to the Terms of Service - if (!body.consent) { - throw FieldErrors({ - consent: { - code: "CONSENT_REQUIRED", - message: req.t("auth:register.CONSENT_REQUIRED"), - }, - }); - } - - if (!regTokenUsed && register.disabled) { - throw FieldErrors({ - email: { - code: "DISABLED", - message: "registration is disabled on this instance", - }, - }); - } - - if ( - !regTokenUsed && - register.requireCaptcha && - security.captcha.enabled - ) { - const { sitekey, service } = security.captcha; - if (!body.captcha_key) { - return res?.status(400).json({ - captcha_key: ["captcha-required"], - captcha_sitekey: sitekey, - captcha_service: service, - }); - } - - const verify = await verifyCaptcha(body.captcha_key, ip); - if (!verify.success) { - return res.status(400).json({ - captcha_key: verify["error-codes"], - captcha_sitekey: sitekey, - captcha_service: service, - }); - } - } - - if (!regTokenUsed && !register.allowMultipleAccounts) { - // TODO: check if fingerprint was eligible generated - const exists = await User.findOne({ - where: { fingerprints: body.fingerprint }, - select: ["id"], - }); - - if (exists) { - throw FieldErrors({ - email: { - code: "EMAIL_ALREADY_REGISTERED", - message: req.t( - "auth:register.EMAIL_ALREADY_REGISTERED", - ), - }, - }); - } - } - - if (!regTokenUsed && register.blockProxies) { - if (isProxy(await IPAnalysis(ip))) { - console.log(`proxy ${ip} blocked from registration`); - throw new HTTPError("Your IP is blocked from registration"); - } - } - - // TODO: gift_code_sku_id? - // TODO: check password strength - - if (email) { - // replace all dots and chars after +, if its a gmail.com email - if (!email) { - throw FieldErrors({ - email: { - code: "INVALID_EMAIL", - message: req?.t("auth:register.INVALID_EMAIL"), - }, - }); - } - - // check if there is already an account with this email - const exists = await User.findOne({ where: { email: email } }); - - if (exists) { - throw FieldErrors({ - email: { - code: "EMAIL_ALREADY_REGISTERED", - message: req.t( - "auth:register.EMAIL_ALREADY_REGISTERED", - ), - }, - }); - } - } else if (register.email.required) { - throw FieldErrors({ - email: { - code: "BASE_TYPE_REQUIRED", - message: req.t("common:field.BASE_TYPE_REQUIRED"), - }, - }); - } - - if (register.dateOfBirth.required && !body.date_of_birth) { - throw FieldErrors({ - date_of_birth: { - code: "BASE_TYPE_REQUIRED", - message: req.t("common:field.BASE_TYPE_REQUIRED"), - }, - }); - } else if ( - register.dateOfBirth.required && - register.dateOfBirth.minimum - ) { - const minimum = new Date(); - minimum.setFullYear( - minimum.getFullYear() - register.dateOfBirth.minimum, - ); - body.date_of_birth = new Date(body.date_of_birth as Date); - - // higher is younger - if (body.date_of_birth > minimum) { - throw FieldErrors({ - date_of_birth: { - code: "DATE_OF_BIRTH_UNDERAGE", - message: req.t("auth:register.DATE_OF_BIRTH_UNDERAGE", { - years: register.dateOfBirth.minimum, - }), - }, - }); - } - } - - if (body.password) { - // the salt is saved in the password refer to bcrypt docs - body.password = await bcrypt.hash(body.password, 12); - } else if (register.password.required) { - throw FieldErrors({ - password: { - code: "BASE_TYPE_REQUIRED", - message: req.t("common:field.BASE_TYPE_REQUIRED"), - }, - }); - } - - if ( - !regTokenUsed && - !body.invite && - (register.requireInvite || - (register.guestsRequireInvite && !register.email)) - ) { - // require invite to register -> e.g. for organizations to send invites to their employees - throw FieldErrors({ - email: { - code: "INVITE_ONLY", - message: req.t("auth:register.INVITE_ONLY"), - }, - }); - } - - if ( - !regTokenUsed && - limits.absoluteRate.register.enabled && - (await User.count({ - where: { - created_at: MoreThan( - new Date( - Date.now() - limits.absoluteRate.register.window, - ), - ), - }, - })) >= limits.absoluteRate.register.limit - ) { - console.log( - `Global register ratelimit exceeded for ${getIpAdress(req)}, ${ - req.body.username - }, ${req.body.invite || "No invite given"}`, - ); - throw FieldErrors({ - email: { - code: "TOO_MANY_REGISTRATIONS", - message: req.t("auth:register.TOO_MANY_REGISTRATIONS"), - }, - }); - } - - const user = await User.register({ ...body, req }); - - if (body.invite) { - // await to fail if the invite doesn't exist (necessary for requireInvite to work properly) (username only signups are possible) - await Invite.joinGuild(user.id, body.invite); - } - - return res.json({ token: await generateToken(user.id) }); - }, -); - -export default router; - -/** - * POST /auth/register - * @argument { "fingerprint":"805826570869932034.wR8vi8lGlFBJerErO9LG5NViJFw", "email":"qo8etzvaf@gmail.com", "username":"qp39gr98", "password":"wtp9gep9gw", "invite":null, "consent":true, "date_of_birth":"2000-04-04", "gift_code_sku_id":null, "captcha_key":null} - * - * Field Error - * @returns { "code": 50035, "errors": { "consent": { "_errors": [{ "code": "CONSENT_REQUIRED", "message": "You must agree to Discord's Terms of Service and Privacy Policy." }]}}, "message": "Invalid Form Body"} - * - * Success 200: - * @returns {token: "OMITTED"} - */ diff --git a/src/api/routes/auth/verify/view-backup-codes-challenge.ts b/src/api/routes/auth/verify/view-backup-codes-challenge.ts deleted file mode 100644
index 65f0a57c..00000000 --- a/src/api/routes/auth/verify/view-backup-codes-challenge.ts +++ /dev/null
@@ -1,34 +0,0 @@ -import { Router, Request, Response } from "express"; -import { route } from "@fosscord/api"; -import { FieldErrors, User, BackupCodesChallengeSchema } from "@fosscord/util"; -import bcrypt from "bcrypt"; -const router = Router(); - -router.post( - "/", - route({ body: "BackupCodesChallengeSchema" }), - async (req: Request, res: Response) => { - const { password } = req.body as BackupCodesChallengeSchema; - - const user = await User.findOneOrFail({ - where: { 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", - }, - }); - } - - return res.json({ - nonce: "NoncePlaceholder", - regenerate_nonce: "RegenNoncePlaceholder", - }); - }, -); - -export default router;