diff --git a/api/package-lock.json b/api/package-lock.json index f73711a..c0498ad 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -25,6 +25,7 @@ "mqtt": "^5.7.0", "mysql2": "^3.9.8", "nodemailer": "^6.9.8", + "otpauth": "^9.3.6", "showdown": "^2.1.0", "ts-node": "^10.9.2" }, @@ -1009,9 +1010,10 @@ } }, "node_modules/@noble/hashes": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.5.0.tgz", - "integrity": "sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.6.1.tgz", + "integrity": "sha512-pq5D8h10hHBjyqX+cfBm0i8JUXJ0UhczFc4r74zbuT9XgewFo2E3J1cOaGtdZynILNmQ685YWGzGE1Zv6io50w==", + "license": "MIT", "engines": { "node": "^14.21.3 || >=16" }, @@ -2242,9 +2244,10 @@ } }, "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -2297,9 +2300,10 @@ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -2790,16 +2794,17 @@ "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==" }, "node_modules/express": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", - "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.6.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -2813,7 +2818,7 @@ "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", @@ -2828,6 +2833,10 @@ }, "engines": { "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/express/node_modules/debug": { @@ -4610,6 +4619,18 @@ "node": ">= 6" } }, + "node_modules/otpauth": { + "version": "9.3.6", + "resolved": "https://registry.npmjs.org/otpauth/-/otpauth-9.3.6.tgz", + "integrity": "sha512-eIcCvuEvcAAPHxUKC9Q4uCe0Fh/yRc5jv9z+f/kvyIF2LPrhgAOuLB7J9CssGYhND/BL8M9hlHBTFmffpoQlMQ==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.6.1" + }, + "funding": { + "url": "https://github.com/hectorm/otpauth?sponsor=1" + } + }, "node_modules/p-map": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", @@ -4745,9 +4766,10 @@ } }, "node_modules/path-to-regexp": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" }, "node_modules/path-type": { "version": "4.0.0", diff --git a/api/package.json b/api/package.json index 87728e4..fa8c671 100644 --- a/api/package.json +++ b/api/package.json @@ -28,6 +28,7 @@ "bcrypt": "^5.1.1", "cors": "^2.8.5", "dotenv": "^16.3.2", + "drizzle-orm": "^0.31.0", "express": "^4.18.2", "generate-license-file": "^3.4.0", "helmet": "^7.1.0", @@ -37,8 +38,8 @@ "mqtt": "^5.7.0", "mysql2": "^3.9.8", "nodemailer": "^6.9.8", + "otpauth": "^9.3.6", "showdown": "^2.1.0", - "drizzle-orm": "^0.31.0", "ts-node": "^10.9.2" }, "devDependencies": { diff --git a/api/src/controllers/auth.ts b/api/src/controllers/auth.ts index 94460f4..ea096f6 100644 --- a/api/src/controllers/auth.ts +++ b/api/src/controllers/auth.ts @@ -4,15 +4,16 @@ import { users as userSchema } from '@/db/schema'; import { eq, and, or, gt } from 'drizzle-orm'; import { hashPassword, comparePassword } from '@/utils/passwords'; import * as email from '@/communication/email'; -import { generateSecureCode } from "@/utils/auth"; +import { generateSecureCode, verifyTOTP } from "@/utils/auth"; import { log } from "@/utils/log"; +import { verify } from "crypto"; const jwt = require('jsonwebtoken'); const dotenv = require('dotenv'); dotenv.config(); const login = async (req: Request, res: Response, next: NextFunction) => { - const { username, password } = req.body; - const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress + const { username, password, totp } = req.body; + const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress // Search for user by username const users = await db.select().from(userSchema) @@ -42,6 +43,31 @@ const login = async (req: Request, res: Response, next: NextFunction) => { }); } + if (users[0].totpEnabled) { + + // Check if user even bothered to give us a totp code + if (!totp) { + await log(`Failed login from IP ${ip} - missing totp`) + return res.status(401).json({ + message: "Please provide a TOTP code", + totpRequired: true + }); + } + + // Verify TOTP code + // const isTotpCorrect = verifyTOTP(users[0].totpSecret, totp); + const isTotpCorrect = verifyTOTP("3UWIINDEILTF67VQ3RVLXSWZZGX5REFF", totp); + console.log(isTotpCorrect, "is totp correct?") + + if (isTotpCorrect === false) { + await log(`Failed login from IP ${ip} - invalid totp`) + return res.status(401).json({ + message: "Invalid TOTP code", + totpRequired: true + }); + } + } + // Create a JWT - make it last for 24 hours const token = jwt.sign({ id: users[0].id, username: (users[0].username).toLowerCase() }, process.env.AUTH_SECRET, { expiresIn: '86400s' }); @@ -53,7 +79,7 @@ const login = async (req: Request, res: Response, next: NextFunction) => { const forgotPassword = async (req: Request, res: Response, next: NextFunction) => { const { username } = req.body; - const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress + const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress // Check for idiots if (username.length === 0) { @@ -105,7 +131,7 @@ const forgotPassword = async (req: Request, res: Response, next: NextFunction) = const changePassword = async (req: Request, res: Response, next: NextFunction) => { const { resetToken, password } = req.body; - const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress + const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress const serverTime = new Date(); // Search for user by username diff --git a/api/src/utils/auth.ts b/api/src/utils/auth.ts index 6b993e8..d732109 100644 --- a/api/src/utils/auth.ts +++ b/api/src/utils/auth.ts @@ -1,5 +1,6 @@ -import e, { Request, Response, NextFunction } from "express"; +import { Request, Response } from "express"; import { randomBytes } from 'crypto'; +import * as OTPAuth from "otpauth"; export function generateSecureCode(): string { const chars = '0123456789ABCDEF'; @@ -45,4 +46,21 @@ export function getTokenFromAuthCookie(req: Request, res: Response) { } return token +} + +export function verifyTOTP (secret: string, token: string): boolean { + console.log(secret) + const _secret = OTPAuth.Secret.fromBase32(secret) + console.log(_secret) + const totp = new OTPAuth.TOTP({secret: _secret, digits: 6, period: 30, algorithm: 'SHA1'}) + const realCode = totp.generate() + console.log(realCode, "real code") + console.log(token, "token") + + const validationResponse = totp.validate({ token: token}) + console.log(validationResponse, "validation response") + const validstep = validationResponse === null ? false : (typeof validationResponse === 'number' && validationResponse < 3 ? true : false); + console.log(validstep, "valid step") + + return validstep } \ No newline at end of file diff --git a/jobs/src/uol-timetable/utils.ts b/jobs/src/uol-timetable/utils.ts index a1c9798..dd9182a 100644 --- a/jobs/src/uol-timetable/utils.ts +++ b/jobs/src/uol-timetable/utils.ts @@ -283,5 +283,11 @@ export function getEventType(rawEventType: string, rawName: string): IEventType returnableEventType.name = "" } + // Make all social events 'social' + if (rawName.toLowerCase().includes("social")) { + returnableEventType.type = "SOCIAL" + returnableEventType.name = "" + } + return returnableEventType } \ No newline at end of file